My biggest problem with the refusal to be memory safe is the fact that those problems end up becoming my problems when I am forced to use these applications and I have to think about how there might be a zero-click zero-day that uses an overflow in some random codec. Not as a software developer, but a regular person I want my application to be written in rust or at least use fil-c at bare minimum.
Now as a software developer I feel like this is even more important because I use libraries maintained by thousands of other developers that might also use applications that have these exploits which get their systems compromised pushing malware to thousands of other developers which end up compromising even more libraries.
I believe that memory safety should be the standard for software that thousands if not millions rely on and that it shouldn't be some political issue of X is better, Y is that, Z is something else.
But then again, social engineering is the primary source of malware spread so I don't know.
It seems to me that memory safety might be the difference between the software engineering and Software Engineering. As in, an actual Engineering discipline.
However, I should probably pipe down, as I would not call myself either one.
> memory safety might be the difference between the software engineering and Software Engineering. As in, an actual Engineering discipline.
I wouldn't go that far, what matters is the finished whole. Memory safety of the finished program is a critical factor and using a memory safe language makes it easier to achieve that goal.
However simply using a memory safe language doesn't make you a "Software Engineer" any more than using a certified I-beam makes someone a "Civil Engineer". What matters is that the finished structure/program meets the explicit and implicit requirements of safety, functionality, durability, cost, etc.
Not to mention that complex reliable systems are usually engineered out of much less reliable components.
There are too many opensource projects that show that even with good engineering discipline humans are flawed creatures.
Memory safety is just little thing that makes sure that when you write code at 4am that it will not leak memory via trivial mistakes such as forgetting to free something, freeing something twice or passing a freed pointer. I believe AI agents shine here the most because the they do not get tired and are getting pretty damn predictable.
I don't think we should set our expectations based on an extreme outlier. qmail is special, and we can't expect most software to get to its level of security/safety.
Put another way: if you have to rely on programmer skill or attention to detail in order to guarantee something, that will always be a losing bet, on average. The existence of a tiny percentage of programmers that can clear that high bar does not make it a valid strategy.
It had one bad cve it seems, but that's exactly what I mean. It only takes one mistake, of course you can learn and never make those mistakes again, however, that is an unrealistic expectation in software that receives hundreds of feature updates a year especially when it comes to core applications as basic as communication when it wants to support image previews, reels and whatnot.
There will always be one, so "it only takes one" is meaningless and invalid. That leaves less is better than more, and any form of less is as good as any other form of less.
Most people I know that had security incidents did not have this because of memory safety issues. But it does not matter, even without memory safety issues out of the picture, you would need to update your software and be wary of supply chain attacks.
(Actually, I can't remember a single incident where somebody I knew was directly affected by a memory safety issue)
If as a user you're willing to pay these library/application owners and premium to do so, by all means; this is a reasonable demand. But short of a massive campaign to educate and change minds, I can't see the average user caring enough.
My biggest gripe honestly is businesses rather than any particular opensource project, opensource projects can often get away with these issues getting caught by the many eyes looking at them before they ever make it to mainline.
> I'm hoping, though, that most of the Rust devs, who say they care about memory safety, genuinely care about making software safer, and not just criticizing languages that compete with Rust.
Well I feel most of the Rust devs are there for saying shits about other languages to prove their own dumbassness.
This is perhaps a different take, but one reason I love Zig and C is because I've learned how to reason in pointers. I've worked with Rust in the past on a ~12,000 LOC side project, so I was all in with tree ownership and XOR mutability. But it turns out there's all sorts of delightful data structures that you can only express with unsafe in Rust. Intrusive doubly linked lists are the coolest thing ever. The fact that I can have a LRU cache that changes what's at the front by shuffling some pointers, while still keeping stable addresses so the hash map stays stable? That's freaking cool. Atomic operations with pointers for linked lists is really slick for implementing an allocator's free list between threads. Being able to walk a live heap using a breadth first search by just... following pointers, made the elegance of Dijkstra's algorithm come alive.
Now, maybe I'm only running into these algorithms because these are the problems I'm running into, but in the Rust community I consistently got the message that linked lists were a legacy data structure. In some cases they are, but when they do apply they do so brilliantly.
I know working in Zig leaves plenty of room for memory unsafety. Perhaps that's a bad thing. But I'm implementing an interpreter, and these algorithms are essential for it to run fast. I really do need to be aware of where every allocation happens, and what instructions the computer is running. So for now I stick with Zig, even though I know I'm opening myself up to memory exploits.
Given you like pointers so much a dislike for Rust would make sense if Rust didn't have pointers, but it does and IMO as somebody who spent years getting paid to write in C, Rust's pointers are better.
I don't know if Zig has made any clear decisions about this (chime in any Zig experts) but in C [and C++] the pointers are crap because they're "zapped" when the thing pointed to is gone. Now of course in reality your compiler won't overwrite your pointer just because the standard says it could, but because the compiler knows it would be allowed to do that, it can optimise pointer tricks in surprising ways. To work around this, C and C++ provide pointer-sized integers and exhort you to do any tricks with those instead because there's no zap.
In Rust that's unnecessary, the pointers are just pointers, if the thing pointed at is invalidated, you mustn't dereference that pointer because it's not pointing at anything now, but you can still for example XOR it against another pointer. Clever pointer tricks are thus your responsibility as programmer, and you can just do them with pointers rather than needing this pointer-sized-integer workaround.
Linked lists are cool but they are terrible for cache performance, so their apparently elegant performance characteristics are often illusory. And hybrid data structures which get you the best of both worlds are quite complicated to implement. I think that's why there's a general pressure against using them unless you have a very specific reason (and ideally some measurements) to demonstrate that they are a good option.
(part of this I think is some C teaching which tended to overemphasize linked lists because they are useful for teaching the concept of pointers, which gave a lot of people learning C the impression that it should be the default way to represent a list of objects, when it's far more the exception than the rule)
Yeah, I'd blame mainly 2 factors for the continued undue influence of linked lists
1. CS professors teach them. The Programme Lead for our CS course still teaches linked lists as the first data structure in the DSA course. Does this type exist? Sure. Is it a good idea? Almost never. But you wouldn't think so from its prominence in the course materials.
2. The Linux kernel uses a LOT of linked lists. Multi-core atomic operations on mutable data, a nightmare you will probably avoid in your software is necessary for the OS kernel and so it has linked lists. Linux is very famous, but your software is almost certainly not an operating system kernel.
> but your software is almost certainly not an operating system kernel.
In general yes, but right now I'm contributing to folk.computer, which is a multithreaded task scheduler/central DB (among other things not relevant to the topic at hand). The biggest use of atomics is during statement insertion and removal, by using RCU operations in the central trie.
One of our current performance drags is the interpreter we use is not threadsafe, so we have to serialize and deserialize objects as they move between threads. This contributes to about 30% of Folk's CPU usage. So I'm currently working on making a threadsafe interpreter by porting the Tcl interpreter we use (Jimtcl), and I'm learning all the fun things around atomic ordering and threaded data structures. So I'm definitely the audience for these data structures.
Traversing a linked list is terrible for performance. But in cases like the cache you are only poping from the front, appending to the back or removing which are all quite fast and require little pointer chashing.
Cache cost of individual allocations can be an issue but that can also be mitigated with a good allocator and even without special care can be quite performant in many applications.
I agree the linked lists shouldn't be the first structure you reach to. But there are situations where they can perform very well.
Well, there is another safe option for flexible pointers if you can compile as C++ [1]. In C++ you can have non-owning "smart" pointers with run-time-checked lifetimes [2][3]. Importantly, there is no run-time overhead to dereference them. (That is for the "never-null" versions, otherwise there's a null check.) Assignment has additional run-time cost, but pointer assignments are generally much less prevalent in performance-sensitive inner loops than dereferences, right?
And you can statically verify [4] your raw pointers, so that you only need to use the run-time-checked pointers for the more exotic lifetime relationships.
(That said, for the situations where Fil-C acceptably solves your problem, it's probably the more practical, complete and well-supported solution. And since not many seem to be explicitly mentioning it, the recent Fil-C demonstration of memory-safe linux userspace is rather impressive, right? I've heard that IT security is a $100B industry. I'm guessing an inappropriately low proportion of those resources are being invested in Fil-C. :)
Interesting, this is pretty cool! I'm guessing it doesn't work with pointer to int casts though? I'm using NaN packing, so I necessarily have to cast it to an integer.
EDIT: though I'm considering switching to a tag + 64 bit union, because of 54 bit addressing and pointer tagging. It makes it tricky to get my data layout right though, since it makes all of the structures larger.
> But it turns out there's all sorts of delightful data structures that you can only express with unsafe in Rust. Intrusive doubly linked lists are the coolest thing ever.
I don't mean this to be a gotcha, but I think it's important to say that we can have these in rust too! The only difference is that the first thing we have to talk about is you the user can go about using an intrusive collection correctly. I love the cordyceps crate for this. If you want to be able to use your type as a linked list node, you must implement a Linked trait, the documentation of which clearly explains how to use it safely: https://docs.rs/cordyceps/latest/cordyceps/trait.Linked.html
I think this crate description encapsulates what's difficult about unsafe rust, which is how unergonomic pointers are. Like why do I need to use `addr_of_mut!`? I'm sure there's a good reason, as Rust tends to think these problems through, but it's very unintuitive compared to returning `&mut`. I feel like I'm juggling way more concepts, which to be fair helps with safety, but it can obscure the algorithm itself.
Does cordyceps have a derive macro? I can imagine that helps a lot with correct implemention, though when it comes to linked lists I can see people wanting to do it themselves.
> I'm sure there's a good reason, as Rust tends to think these problems through, but it's very unintuitive compared to returning `&mut`.
The addr_of_mut docs [1] give a pretty decent explanation of its reason for existence; in short, it lets you get a pointer to something without needing to create potentially-invalid intermediate references. It (and &raw) probably aren't going to be needed if all you need is a &mut, though.
> Does cordyceps have a derive macro?
Doesn't appear to from a quick glance, and given what's in the safety section of the docs [2] it'd probably need to be marked unsafe [3]. Unsafe attributes are new to Rust 2024, though, so that might be a bit new.
That's true but misleading. I'm pretty sure the question was why a normal reference is bad. You don't need `addr_of_mut!()`, but you do need `&raw mut`.
Rust allows you to use a literal iirc like r#raw, which helps with migration. This isn't too different from Zig's @"var" for example, which I'm glad modern languages have an escape hatch for naming things that don't parse normally.
> what's difficult about unsafe rust, which is how unergonomic pointers are. Like why do I need to use `addr_of_mut!`?
Things are slowly getting better here! '&raw'/'&raw mut' reference operators were stabilized a couple years ago.
Another ergonomic improvement in the pipeline is a better way to access fields behind pointers, something like C's -> operator. This is taking some time to design because there are things other than raw pointers that would benefit from a generalized field projection mechanism, like Pin, NonNull, and (potentially user-defined) smart pointer types.
> Like why do I need to use `addr_of_mut!`? I'm sure there's a good reason, as Rust tends to think these problems through, but it's very unintuitive compared to returning `&mut`.
That documentation appears to be out of date. The `addr_of_mut` macro was elevated to a first-class language feature, `&raw mut` (there's also a corresponding `&raw` operator). These two operators differ from the usual `&mut` and `&` in that they create raw pointers rather than references; prior to the introduction of this feature (or the aforementioned macros that served as precursors) to create a raw pointer you might need to have done a cast like `&foo as *const` in order to create a raw pointer by casting from a reference, but this could have safety consequences if the temporary reference was to an invalid object. Therefore `&raw` and `&raw mut` were introduced to create raw pointers directly without introducing an intermediate reference, which was arguably the most subtle footgun in unsafe Rust for a few years, and it's nice that it's now addressed.
I absolutely agree learning C is a good thing! In fact, there are two camps of Rust people: one that argue that you should not learn C before learning Rust, and one that argues that you should.
Also, while such data structures/algorithms are rare, and more importantly, they can be written once and used many times, someone still needs to write them!
Thank you! I've seen the latter two mentioned in the past, but I never got very far into the books before getting confused with all the moving pieces. I think now that I've worked a lot in languages that put structs and pointers in the forefront, I'd understand it a lot better. Perhaps I'll have to go back and give them a proper read this time...
> The fact that I can have a LRU cache that changes what's at the front by shuffling some pointers, while still keeping stable addresses so the hash map stays stable? That's freaking cool. Atomic operations with pointers for linked lists is really slick for implementing an allocator's free list between threads. Being able to walk a live heap using a breadth first search by just... following pointers, made the elegance of Dijkstra's algorithm come alive.
I feel like these kinds of things should be transformations that the compiler can use to turn Provably Safe code into performant code, i.e. you write the safe code and, once the borrow checker is happy, the compiler does its magic. I have no idea how to codify any of it into a compiler pass, or whether it's actually possible...
There's some interesting work going on with this with Metamath Zero[0], where he's working on verifying everything down to the compiler, to make sure the compiler always emits correct code. His language, Metamath C, also allows for refinement types, which means you can prove certain properties. It's still in its infancy, but I've been watching it from the sidelines for a bit now.
Though in terms of practicality, Graydon Hoare has mentioned in the past that he isn't a big fan of complex systems, and so it's better to have a simple type system that covers 95% of the cases, and an escape hatch when it doesn't. I think that's still a fair assessment, but I've fallen down the proof rabbit hole so I am curious to see how far it could be pushed.
Sadly due to cache friendliness, current conventional wisdom is that linked lists are so slow that a dynamic array of pointers is almost always faster even in cases when you'd think linked lists would be faster, such as frequent insertions in the middle.
You are right on the money! I have been saying this for a while.
Apart from the utility, it is also a lot of fun. So it would be a pity if people are told that these things are legacy and they should not try to use/implement them..
Yeah, probably not. I wouldn't mind fuzzing it with Zig-Fil, but the interpreter (Zicl) is embedded in a larger C project that uses a lot of dynamic linking, so chances of using it in production is pretty much zero. I do have a lot of asserts that only have a small overhead, so I'll probably use ReleaseSafe most of the time, since it does catch a lot of the issues.
See now this is why I don't quite get everyone jumping on the rust bandwagon.
This is a limitation of the rust model. You could have a language where intrusive linked lists are provably safe.
That's not to say rust doesn't work, but it's the first language I'm aware of that's attempted compile time memory safety, and no body seems to be talking about how to do it better.
Personally, I see these language fights as being a bit pointless. They are trying to optimize at the wrong layer. Unless one is working with a dependently typed language, which requires a proof assistant to discharge type checks, then it's all just a question of where you make the trade-off. Don't care about memory leaks or deadlocks as part of your soundness guarantees, but can't have GC? Use Rust. Okay with runtime overhead to verify checks? Consider fil extensions.
If you want to go deeper, skip the language wars entirely. Tooling does what these languages can't do. I have had great success model checking C with CBMC. Not only does this prevent memory safety issues (including memory leaks), but this also prevents deadlocks. Bonus: you can write user contracts and invariants to verify that every execution path of a function fulfills these contracts and invariants.
Kani comes close with Rust. It doesn't yet have decent concurrency support, but there is nothing that prevents this from being added in the future. The ability to write custom contracts and enforce custom invariants more than makes up for its lack of concurrency support. Similar technology could be used or adapted for Zig or C++.
Use whichever language you like. Just, please look into model checking it. The technology scales just fine, once you get over the learning curve and learn how to use compositional verification.
Pizlo is not a memory safety absolutist. His rhetoric towards rust is a tactic specifically designed to draw more attention to him and his project. It is amplified by people who already had a bone to pick with rust and take joy in giving rust folk "a taste of their own medicine," so to speak. Articles like this are taking the bait.
I don't think it's too weird of a take, we do things for many reasons. I get that it feels bad to have that directed at you as an accusation. It's rational to discuss the intent behind actions, but I think the parent is hinting at "so we shouldn't take what Pizlo says seriously", which isn't a good conclusion.
Not so much about language design as how easy it is to take a defensive position on something we are comfortable with when it's challanged. This prohibits growth.
When someone suggests you have been doing something the wrong way or has a new idea, it's usually better to hear them out and take what knowledge you can from them, even if they weren't actually trying to help you.
When you allocate space with Fil-C's "malloc", some additional bounds checking data precedes the space the program gets to use. Pointers are "fat pointers", with a pointer to the beginning of the buffer and a pointer to someplace within the buffer, allowing ordinary C pointer manipulation.
There's much new verbiage around this. But it's roughly the same idea as GCC "fat pointers".[2][3] So it's not a new idea. It's one that's been tried several times, but never caught on.
There's a performance penalty. Especially if the compiler can't hoist the checks out of loops.
Fat pointers show up inline in memory, which has a bunch of problems:
- sizeof(void*) changes
- either you let the bounds get corrupted by bad casts, unions, and other issues, or you impose restrictions that prevent unions from working compatibly, or you end up supporting unions by having issues with races, or you need special hardware. Invisicaps sidestep all of those issues
The "flight pointer" looks an awful lot like a fat pointer. See the diagram at "The intuition of inviscaps" in [1]. A "flight pointer" has a pointer to the beginning of the buffer, which they call the "lower bound ptr", and a pointer to someplace within the buffer, which they call the "integer ptr". The pointer to the beginning of the buffer lets the checker find the size of the buffer, which is stored preceding the buffer. The compiler has to arrange things so that the lower bound ptr is carried around wherever a "flight pointer" goes. This is supposed to be invisible to the programmer.
other implementations of fat pointers (that I’m aware of) have no distinction between flight and rest; they store what I call flight pointers in memory literally.
The closest technique to invisicaps is softbound, but that has issues that invisicaps resolve (better story for races, more comprehensive safety for all of the C and C++ languages, no need for large virtual memory reservations, and lock freedom)
> The thing is, programs that can be also written in GC powered languages, often don't need unsafe at all
That's true, though it's worth noting that they do still need unsafe for the FFI. Often this is where the memory safety issues arise in GC languages. So no language is truly "memory safe".
Which is why in some OS written in memory safe systems languages, like Burroughs, still being sold by Unisys as ClearPath MCP, any use of unsafe code blocks taints the binary.
Execution is only allowed after the OS admin adds the executable to a specific process white list.
This is similar to how some managed runtimes work, and the Java ecosystem is moving forwards it.
Currently it only triggers a warning, however in the future JNI and Panama will be disabled by default, and like with ClearPath MCP, it is up to the person installing the application to enable the unsafe layer explicitly.
Another example is the capabilities approach on WASM runtimes, here again the one executing the platform takes ownership on enabling unsafe behaviours.
I don't think you need to be an absolutist or only use memory safe languages but it's very obvious that we need to do a whole lot better than we actually do. Rewriting in Rust or any other language is one way to do that, but not a perfect one. I'll accept C++ when most C++ software has 1 in 50 chance of an RCE and not just a 1 in 50 chance of a known RCE. I think we can get there but we are not currently there.
Coreutils has a good track record. Ffmpeg doesn't. They should have rewritten ffmpeg in Rust, not coreutils.
There are other approaches though like formal verification.
If you go by this metric (has a RCE and not known to have RCE), I'm pretty sure the actual statistics are more like 1 in 2, at least. And probably more than one RCE.
Didn’t the primary maintainer already signal burnout or frustration or something like that? If an AI company actually did port ffmpeg *successfully* (not like claude CC), the dude might get the release he might have been yearning for
I think the "Fil-C is safer than Rust" thing is an understandable reaction to 10 years of Rust evangelists telling people they have to use Rust otherwise they're stupid and wrong.
The reasonable reaction is to ignore the vocal minority.
It’s tiresome when other language communities start engaging in a battle with a vocal minority of another community.
Every language has its annoying maximalist pushers. The play is to ignore them and do what decisions are best for the language, not to let yourself get dragged down into petty language wars where nobody wins.
We can learn a lot about memory safety from Rust. But if we can achieve the same dream of Fil-C for other more common languages that need it, Rust's popularity will likely wane.
I doubt it. I am a big fan of Rust's memory safety, but it's not just memory safety that draws me to Rust. Having a strong, featureful type system is also a big plus for me, and even if Fil-C could someday magically compile any program or library without modification, and its performance trade offs could be mitigated, I'd still write Rust, because C is miserable to write. (And I say this as a 25+ year C programmer.)
Fil-C has a large performance impact though, and it'll always be reasonably significant. Rusts popularity has always been the combo of safety and performance, otherwise you might as well just use C#
> Rusts popularity has always been the combo of safety and performance, otherwise you might as well just use C#
Combined with the ability to compile to a single native binary, having a solid package management solution, a fairly advanced type system, and a solid selection of nice language features. Usually you need to give up at least one of those.
Had some headaches with nuget, and C# type system isn't as advanced as Rust or TypeScript, even less so in comparison to Haskel (still solid notheless), but it really is a solid choice. Specially in comparison to Java.
> Combined with the ability to compile to a single native binary, having a solid package management solution, a fairly advanced type system, and a solid selection of nice language features
Good article. Not much to add to it, other than I think more people should look at modal type systems like found in Scala 3 and OxCaml. If you want safe arena allocation they are a lot more ergonomic than Rust's approach.
I don't the fanaticism around memory safe languages. We've had memory safe languages for a very long time. Algol-60, the granddaddy of many modern languages, had it sixty-five years ago. Algol-60 also had other safety features that modern languages don't have - for example integer overflow safety.
The real issue is that we came to accept unsafe languages and are taking a really long time to put such features back.
The real issue is effort. We accept ffmpeg because nobody tried to rewrite ffmpeg in ALGOL-60. We're happy to go through contortions to make ffmpeg or sqlite usable from Java because it's useful and not written in Java and nobody wants to rewrite it in Java.
The article points out what I think is the crux of the discussion: With Fil-C they become crashes. Errors manifest at runtime, not at compile time.
I realized some time ago when I talk to people about rust, I don't really mention memory / thread safety in a security context, I mention them in a reliability context. Compile-time checks mean I basically never spend time debugging runtime crashes, and generally can count on the compiler to catch memory and thread safety issues before the program ever runs, and this saves me time and aggravation in production. I care about security, but I care more about reliability, and rust compile-time checks mean my software is reliable in a way that runtime-checked languages just aren't.
This applies to lots of languages. Every time I hit a runtime crash in python, or ruby, or js I die a little inside. Even though they are memory-safe because they're garbage collected, I still waste time debugging runtime errors.
I think Fil-C ABI implemented by all of C, Rust, and Zig is the future. But that would need some sort of stability for interoperation, and stabilizing ABI takes time: Rust still doesn't have one (while Swift spent enormous amount of effort to have one). Experimental implementation is probably still worth doing.
It currently is at least associated with C in the sense that it takes C code as an input. But yes, I would be very happy to see its approach applied to more languages.
I was not aware of the antagonism from the Zig / Fil-C people to Rust, but it makes absolutely no sense.
Like, of course you can prevent all memory safety errors by using a garbage collector. That has been known since the 90s. In fact, there was a good two decades after Java released where all the research on memory safety just stopped, because the standard answer became "use a GC". If you couldn't use GC, you were stuck with languages made prior to Java - which in practice meant just C - or the one language everyone tried to staple every new programming paradigm onto, C++.
In fact, part of why C++ became such an untameable beast of a language is because it became load-bearing for non-GC projects. Anything not in the ISO C++ standard was, in practice, something you just couldn't do in native code. Oh, of course you can't introspect structs in a compiled language, of course macros are useless and unhygenic, of course templates have to be monomorphized and bloat your binary size. And so the pressure for C++ to be an all-singing, all-dancing, all-dressed language grew.
Rust's big story is memory safety, but the more Rust I wrote, the more I realized that the memory safety is only part of the picture. Rust has a very nicely curated selection of features that allows the language to remain understandable despite the sophistication of the compiler. Memory safety is a selling point, sure, but it is also the lubricant that makes working with those features pleasant.
If you want a real complaint about Rust, it's that some features are oversimplified in ways that make certain scenarios harder and make some features way more "magic" than they should be. Have you ever tried writing Futures code without making use of the async keyword? It's nearly impossible, for several reasons; the main being that Rust's type systems cannot express self-referential borrows. This also makes returning a reference to something in an Rc or RefCell you own more difficult[0]. And there are numerous other states memory can be in that are hidden from Rust's type system. Rust can't even represent a real destructor fn. Dropping a value multiple times, or using it after it's been dropped, is explicitly forbidden; but Drop impls still can't take values out of themselves because Rust doesn't have an "owned reference" - i.e. memory you can take values from but can't deallocate.
But none of this compromises memory safety - it just makes certain things harder than they should be.
[0] Strictly speaking, there's an owning_ref crate that manages this; stdlib is also working on a "mapped mutex guard" type that would do the same thing without a dependency.
> I was not aware of the antagonism from the Zig .. people to Rust, but it makes absolutely no sense.
Lol, have you ever watch Andrew Kelley speak? I've only seen 3 or 4 talks he's given, but he frequently makes not-so-subtle digs towards Rust. Zig's home page prominently displays "Focus on debugging your application rather than debugging your programming language knowledge" which sounds horrible to me, but is clearly an anti-rust statement. Competition is good, and Rust and Zig cater to different people, but when the primary personality behind Zig is leads the way, it shouldn't be a surprise that some tribalism-influenced followers lean hard into it.
“Focus on debugging your application rather than debugging your programming language knowledge” to me sounds like it is directed primarily at C and C++., it’s a much more direct translation.
> "Focus on debugging your application rather than debugging your programming language knowledge"
This sounds like it's made by someone who fears discovering they don't know something more than discovering they made a mistake.
I am the reverse: I would prefer to debug my programming language knowledge over debugging my application. I hate debugging applications specifically because making a mistake is SO much worse to me than discovering there is a fact I didn't know yet.
And, of course, debugging your application is discovering there are unknown unknowns in deployment, the worst of the quadrant of knowns and unknowns.
The thing that has changed is that there are spaces where security is being taken more seriously, and C's memory unsafety became a deal breaker there. I do think that providing a mechanism to run existing C software that isn't performance sensitive in a way that mitigates its limitations is very worthwhile.
I think the "static analysis" and the "runtime checks" approaches are complementary, not in opposition, making any noise around having to choose one or the other moot.
> Like, of course you can prevent all memory safety errors by using a garbage collector. That has been known since the 90s.
No, it's been known since GC was invented.
> In fact, there was a good two decades after Java released where all the research on memory safety just stopped, because the standard answer became "use a GC".
Yeah, my understanding (which is not from first-hand experience) is that a huge part of the origin of Rust was that there was a lot of research in memory safety techniques that had been going mostly unused by mainstream languages, so the idea was "Why not try to make a language using some of it?"
AT&T was already doing that as language to replace C, Cyclone.
AT&T even had a full OS, where C only had minor role, the microkernel and Dis VM/JIT, with the whole userpace implemented in Limbo, designed by the creators of UNIX and C.
Good reminder that memory-safe alternatives to C aren't new. Rust may have won more because of ecosystem and timing than because it was the first to address the problem.
All the safety improvements of 21st century wannabe C replacements, were already available in NEWP(1961), PL/I (1964), Mesa (1976), Modula-2 (1978), PL.8 (1982), Ada (1983), Object Pascal (1986), among many other lesser known ones.
However none of them had an OS available in source code for the symbolic price to send tapes around to universities.
Additionally, the remark that the UNIX and C authors themselves did not stop there, and their latest mark on the computing world was the Go programming language, after Limbo, and the acknowledgement that designing Alef without a GC was a mistake.
Probably they would not had to reboot Plan 9 OS design into C.
*nod* They're from before I started bookmarking rigorously, but in the early days of Rust, there was a sentiment in the blog posts from the people developing it that the point of Rust was "to give good ideas a second chance" and that Rust was intentionally boring and un-innovative.
> you can prevent all memory safety errors by using a garbage collector
Garbage collection and a higher-level language that controls allocations handles 2/3 of the memory-safety problem space (using memory you shouldn't via use-after-free, or using memory you shouldn't before allocation/via arbitrary address access), but the other 1/3 isn't addressed by GC: out-of-bounds access on properly-allocated structures.
GC-or-not doesn't stop a pointerful language from addressing memory off the end of an array, and the best techniques we have to deal with that today are imperfect (but still pretty good!) compiler inference for BCE, memory protection to limit the scope of possible overreach, or slowing down runtime by inserting checks.
The rest of your points are well-taken; just pointing out that GC isn't the solution to all causes of memory-unsafety, just 2/3 of 'em.
You forgot the other three thirds GC doesn't handle: in-bounds writes to memory currently in use by another processor. One of the basic ideas behind Rust's memory safety story is that eliminating race conditions necessarily requires a good memory safety story, especially regarding temporal memory safety.
...of course, the managed code languages had an answer to this: put a lock on every managed object you allocate.
But they didn't actually make you acquire the lock - it only comes into play if you declare a method to be synchronized or if you acquire the lock externally. Which is a problem because most of these languages ALSO are designed to act as a security boundary for untrusted code - something Rust doesn't even offer! Which means even though the language doesn't enforce race-freedom, it has to enforce enough of it anyway to keep racy code from corrupting the language heap (which is now security-sensitive). No other language (not even Rust) does this - if you write a race in unsafe Rust, it is actually UB, but in Java you actually still have some guarantees about what your program will do.
Actually, let me throw a bone to the Zig people: Unsafe Rust actually imposes more UB than C does, and it is more difficult to write sound unsafe Rust. If you're writing FFI code[0], you're probably fine. But if you're writing new memory abstractions, you're probably going to run into Rust's requirement that all mutable references be exclusive. If this ever fails, your program is already in UB and all bets are off. Morally speaking, Rust sprinkles the equivalent of C's restrict keyword over every &mut in your program. In fact, this UB is so strict that the Rustc people have had to turn this on and off multiple times in the past because LLVM would miscompile sound / safe Rust code if it was told about the aliasing restriction.
In practice, however, this doesn't really matter. Rust has all sorts of soundness holes nobody would ever trigger by accident. There's a long standing trait-handling bug that lets you confuse memory types in safe Rust; and any OS that exposes process memory as a writable file lets you do the same thing. In Java, at least the former would be a CVSS 10.0 security vulnerability, but it doesn't matter in Rust, because safe Rust is not a security boundary. It's a set of tools to keep you from shooting yourself in the foot. And, in practice, Rust does a pretty good job of that.
[0] Or more generally, unsafe code where you have two unsafe pieces that you have to guarantee are used together. For example, if you have a C-style callback API that takes a function pointer and a data pointer, and then calls the function with the data, you have to use Box::into_raw() and Box::from_raw() to maintain ownership over the data. from_raw is unsafe, but all the obvious ways of using it are sound.
Fearless concurrency only works as advertised when those data structures aren't exposed via the MMU to other processes, or are handles to resources like files or database connections, where each thread can do whatever they want without type system checks.
All relevant scenarios when the goal is systems programming.
The main problem of Fil-C or similar solutions is not that they provide absolute safety with no escape hatch (unlike languages with unsafe keyword). The problem is that they provide an excuse to keep using terrible programming languages like C and C++ allowing memory safety issues in the first place.
In case of such projects like LLVM C++ usage can be tolerated. But for new code or smaller codebases a better alternative should be considered.
Also Fil-C can't be used for LLVM anyway, since performance and memory consumption overhead is way too much. Nobody wants clang/rustc working 4x slower and consuming 2x more memory.
Hence why somehow fixing C and C++ is also quite relevant for the upcoming decades, assuming humans still matter on the planet.
Now it is going to be ARM MTE, SPARC ADI, CHERI, Fil-C, or WG14 and WG21 actually getting their act together, remains to be seen, as it is subject to governments and industry pressure, and we not destroying civilisation.
Isn't needed if we rewriting anything in Rust anyway.
> GNOME, KDE
They aren't that huge. Huge is the overall codebase of applications using them. It's relatively easy to create a native Rust GUI framework and rewrite applications needed such a framework one by one.
> Linux kernel
It's a good opportunity to rewrite it. Not only because modern languages like Rust are better than C, but because such a rewrite allows getting rid of old stuff present only for historical reasons.
> BSD variants
Aren't needed if we writing a new kernel from scratch.
> CUDA, OpenCL
Not a huge problem. There are dozens of GPU-related languages. Adding one more isn't that problematic.
> Vulkan, DirectX, Metal
They are language-agnostic (but still unsafe). One can create an implementation in something other than C.
Once again, people are grappling with an axiomatic definition of "memory safety" and avoiding the fact that it's a term of art with a very specific meaning: comprehensive protection from The Memory Corruption Vulnerabilities, which include overflows, the lifecycle vulnerabilities like UAF and type confusion, and uninitialized variables.
To the extent Fil-C and Rust both address these vulnerabilities, and don't include design features that in any practical way admit them, they're memory safe. What always feels like is missing from these kinds of analyses is that there are lots of memory-safe programming environments. Almost every Java, Python, Ruby, Javascript, and Go program is memory safe, in the real meaning of the term.
The competition to lock in and promote adoption of the "most" memory-safe language is a category error... unless you do what every language-war argument does, and redefine the term.
Even within Rust some folks do this to themselves, misusing unsafe with code that is 100% safe Rust, only to taint code that should only be called in specific cases, completely unrelated to memory safety.
I never seen this happen with GC/RC languages that also support unsafe code blocks.
I don't particularly care about Rust vs Fil-C shit flinging, I don't even see them as competing since they have effectively near opposite tradeoffs.
You could even use Rust alongside Fil-C to ensure the unsafe blocks are safe. It would even be interesting to turn off some of Fil-C's expensive protections in the safe parts of the Rust code, making an average-of-both-worlds sort of solution.
What I do care about are C and C++, these two absolute garbage languages (though I do have a lot of love for C, you can both love and hate something).
Tools like Fil-C, hardened mallocs like SlimGuard, and other "make C safe" solutions all sacrifice absurd amounts of performance because these two moronic languages refuse to have a proper slice/span type.
Use-after-free and double-free are serious problems but they're a spec of dust compared to missing bounds-checks. The low-hanging fruit is right there for the picking but everyone is worried about how to reach the fruits at the top.
C++ only now in C++26 is finally adding bounds checks to the [] operator on std::span and (hilariously) is also finally adding the now mostly redundant .at() method which should have been there from day one.
Clang added -fbounds-safety which is a feature that should have existed for a long time and serves as a decent stop-gap to a proper slice type. But of course, since it's not standardized, most people won't use it.
What these two languages have taught us is that if you want to produce utter garbage you should make it an ISO standard.
Me, personally, I find this whole discussion on memory safety amusing. Missing bounds checks are by far the biggest source of vulnerabilities and they're a problem in exactly 2 languages and those 2 languages have outright refused to do anything about it for decades.
But hey, even in memory safe languages you have frameworks like log4j that had remote code execution from a format string as a feature. Is the problem memory safety specifically, or this utter cavalier attitude towards security?
I'm not even suggesting something dumb like "you just gotta get good at C and then you won't have any vulnerabilities". Humans are fallible, which is why we delegate what we can to machines. I'm just pointing out the utter lack of care. People just don't care.
At least do the bare minimum of effort like, I don't know, not make remote code execution a feature tied to format strings. Or provide a slice type in your language and string manipulation functions in your standard library that make use of it, preferably 20 years ago.
C++ has had bounds checking in standard library for ages, and on compiler specific frameworks like MFC and OWL, however plenty of devs apparently never read the compiler manuals on how to improve safety and related compiler settings.
It is also relatively strange that many devs use the lacks of standardisation as an excuse to not adopt safety features in C and C++, while at the same time rush out to adopt cool toys that are specific to a single compiler.
Partially yes. Fil-C goes further in creating an entire ecosystem of Fil-C compiled "legacy" libraries, meaning all the unsafe FFI your Rust code does into C is also safe, and even many linux syscalls.
Miri is meant as more of a sanitizer type tool that you use during development to catch bugs. Fil-C is a platform you compile your entire Linux distro with for use in production.
As long as the corporates continue to erode personal freedom with their hostilities, I consider these "memory safety absolutists" authoritarians, because humans will always make mistakes one way or another, and that should be the natural state of things, as attempts to banish all human error under some guise of "safety" will eventually lead to a horrible dystopia.
"Freedom is not worth having if it does not include the freedom to make mistakes."
"They who can give up essential liberty to obtain a little temporary safety, deserve neither liberty nor safety."
Something that is important to me is that it's caught at compile time. A memory issue is a logic bug. Fil-c simply moves that from undefined behavior/security issue/incorrectness to a crash. That's better than what it was. But I generally don't want my programs to crash. Having memory safety at compile time is much more worthwhile imo.
As a Rust fanboy, I have to concede that Rust also doesn't have pure compile-time memory safety. Some checks are runtime there as well, in particular most of bounds safety.
The important thing from a performance standpoint is to be able to hoist bounds checks out of inner loops.
This avoids a check on each iteration.
Languages which have constructs such as
for foo in bartab {}
can usually hoist checks out of inner loops almost for free.
I used to argue with the C++ committee about to when it's OK to catch an error early.
If you write
#define LEN 1000
int tab[LEN];
for (auto p = tab; p++; p <= LEN) { *p = 0; }
that's a buffer overflow. The question is, is the compiler allowed to generate checking code which will abort the program before entering the loop? Or does it have to execute all the iterations up to the subscript error?
I argued that it's legit to catch an error at the point it becomes inevitable.
This leads to bikeshedding objections: "But what if a signal interrupts the loop before it runs off the end". That's why you need a language where undefined behavior has been nailed down to do this optimization properly.
True... but if you go by what you're most likely to encounter, I'd argue the biggest unaddressed flaw in Rust is the lack of a current, maintained analogue to tools like Rustig! and findpanics, which would analyze your compiled, optimized binary and report any code paths which lead to panics.
I don't like having to contort my code to fit each unit of work into the API of `std::panic::catch_unwind` so I can responsibily distrust the transitive dependencies beyond the reach of my Clippy lints.
If you really believed that, you’d be programming in something like ATS, Idris, or Spark Ada. The reason that (I’m guessing) you don’t is that it takes a lot more effort to write general purpose code in those languages.
Rust is pretty much state of the art in this area for general-purpose, non-GC programming languages, and people still complain about the effort involved in writing memory-safe code with it.
If you really want complete memory safety, use a garbage collected language.
I program mostly in Rust, Haskell and Python nowadays. I indeed would love to go gc-less dependently typed. But I work in a team where the code I build is expected to be maintained by other devs. And going that route is unfortunately not tenable.
It’s the exact opposite for me. Catching it at compile time is great, but the most important thing by far is that it’s caught somewhere instead of creating a security issue.
I don't know if there can be any memory safe languages.
Programming is always unsafe because you can always make mistakes.
The best we can do is to use tools that help us avoid the common mistakes.
As an example of that, the rust compiler helps the programmer to avoid many mistakes but it's still possible to use memory incorrectly and the tool is only safe as long as you use it as intended.
You could say that C is safe too as long as you use it as intended and make no mistakes.
The only problem is that it's hard to use it as intended and make no mistakes.
So let's think about what memory safety could mean.
My understanding is that a memory bug is when you use memory in an unintended way.
An example of that is when you write to memory after you have deallocated it, which means after you have decided not to use it any more.
No compiler or tool can make sure you don't write to or read deallocated memory, because whether it's allocated depends on what your intention is, and no compiler can read your thoughts.
They can only give you a tool (like functions for allocating and deallocating memory or compiler checks) and hope that you will use that tool in a way that reflects your intention.
One problem is that those tools are not always sufficient to keep track of your intentions and you may not always use them as intended.
Memory allocation is relative.
In a sense, no program that runs under an operating system can use deallocated memory, because when they read or write to memory that has not been mapped to the process, they crash.
In a sense, even "safe" rust can use deallocated memory;
let's say you have an array of 10 integers and you decide that the fifth integer should not currently be used, you have then deallocated it on a level where the available tools can not help you, but you can of course still access that memory.
So again, everything is safe if you use it as intended and nothing is safe if you use it in other ways.
Safety depends on how well the code matches your intentions.
Remember that programming always happens in layers and no tool can cover all of them.
It's not even clear what "all layers" would mean or how many there are in a program.
> but it's still possible to use memory incorrectly
Only by misusing unsafe. Using it in regular programs actually isn't that necessary. In languages like C you have unsafe code almost in every line.
> My understanding is that a memory bug is when you use memory in an unintended way
Memory safety rules are more strict and formalized than you think. Memory safety issues are typically reading uninitialized memory (or strictly speaking changing observable behavior based on contents of uninitialized memory), reading/writing memory after it have been freed (free/delete call or out-of-scope going for local variables), concurrent unsynchronized memory access.
> No compiler or tool can make sure you don't write to or read deallocated memory
I am author of a programming language, where you can't write to or read deallocated memory, unless you misusing unsafe.
> no compiler can read your thoughts
But many languages more complex than C have powerful features allowing telling the compiler your about intents. At least partially.
> Memory allocation is relative. In a sense, no program that runs under an operating system can use deallocated memory
You are mixing two different concepts. One is memory model of the abstract machine defined by the specification of a language and other is the OS processes model. They have much in common, but there are a lot of differences.
> So again, everything is safe if you use it as intended
Such mindset is considered harmful, since it provides an excuse to use languages where making mistakes is easy (like C).
I think the point you missed or don't want to accept is that I think of all layers of memory management as the same thing just on different layers.
Where you say that I mix different concepts, I say that it's just different layers of the same problem.
So which layer should we care about?
All of them?
Yes, if we want the most safety.
I will give you an example of how I can use memory incorrectly in "safe" rust.
Let's say I have an integer that contains my age.
Now I forget what the number was used for and I use it as shoe size.
I have now used memory (the content of the variable) as something it was not intended for.
Maybe you think this example is silly, but what is really the difference between using a float pointer as an int pointer and using age as shoe size?
Another thing we could do is to use an index for one array as an index for another array.
That's an example of using a pointer as the wrong type because an index is just a relative pointer.
So if we want to care about all layers of memory safety then we have to care about not using ages as shoe sizes and indexes with the wrong arrays.
"Memory safe" languages usually don't help us with that and therefore they don't give us total memory safety.
I think "memory safety" is not a very useful term because all data in a program lies in memory and basically all bugs have something to do with using that memory in wrong ways.
> we have to care about not using ages as shoe sizes and indexes with the wrong arrays. "Memory safe" languages usually don't help
In languages even slightly better than C one can create a wrapper type for int/float with additional semantics like age, shoe size or something else. Since they are different types, using one in place of other isn't possible. This doesn't solve all problems, but at least can prevent silly mistakes.
Interesting, I have had ideas about having that feature in a language: being able to make subtypes, like "this is an integer but on the next level it is a shoe size".
Can you give an example of such a language or how they usually do that?
The problem with this post is the extent to which it shows that its author has an unhealthy obsession with me personally. It’s weird but also oddly flattering.
I don’t dislike Rust, and when I point out that Rust is not fully memory safe, it’s because I find the details here super interesting. It’s interesting that Rust deliberately chooses to have an unsafe subset. It’s interesting how that leaks out to the rest of the language. It’s interesting because us language designers ought to be thinking about how this could be avoided. It’s a hard problem! And that makes it fun!
If you do read what I say on twitter, you’ll find praise for Rust, many concessions about Rust being faster than Fil-C, as well as a wide range of other opinions. When I point out Rust’s unsafety, I’m just citing facts. It’s interesting how doing that really seems to upset some folks.
Pointing out a limitation in a technology does not imply dislike, and it’s best not to take it personally.
I don't have an obsession with you. I haven't read the entirety of your social media presence, but almost every time I see a discussion about Rust on Twitter, especially involving Fil-C, I see your criticism of Rust, without much nuance. To be honest, seeing your replies emphasizing Rust's memory unsafety under pretty much every tweet I've seen about the subject felt like an obsession with Rust, to me.
The problem I have with the framing I've seen countless times from you on social media is that I have never seen any nuance on what Rust's memory safety model achieves. You say you state facts, but stating facts without context can still be dishonest. Claims like "both C and Rust are memory unsafe languages" or "both C and Rust allow introducing memory safety vulnerabilities" are factually correct, but pretty much useless in practice.
I'm sorry for misinterpreting the "dislike" part. In retrospect I shouldn't have even brought like or dislike into it cause it was not the main point anyway. The main point was framing that I've seen on Twitter, not only from you, that in my opinion is quite one-sided.
As someone who really enjoys seeing all of the different programming languages and their approaches to different problems, it’s becoming obnoxious to hear the constant battles among people who think programming languages need to become part of your identity. The endless battles of superiority and tit-for-tat responses feel petty and distracting.
It’s refreshing to get back to people who just want to try different things and experiment without making everything into a battle where each side scores points against the other.
We did that to ourselves, picking languages like football memberships assigned at birth, with religious fights on the playground.
Not only languages, editors, OS, hardware platform,...
Because when it comes to apply for a job, the HR drones don't understand that one can manage to program in more than one language, use more than a specific OS, and so forth.
It was to be those specific bullet points, and by the way at least during the last five years exactly before applying to the position.
There are also the trivia questions during the interview about obscure language features.
So this naturally spills into one's identity, going back to the primitive mind of the days humans lived in small groups and it was us versus them across all tribes.
Which for those of us that are generalists, is a big pain in some body part.
Memory safety can be defined in more than one way, so it’s super worthwhile to figure out how to define it and what meets the definition and what doesn’t.
We should do more of that as a community. It’s important stuff. Ima do my part so you’ll likely see me poke at how Fil-C does a thing that Rust doesn’t do. If you read my arguments unemotionally, I think you’ll get a deeper appreciation for Fil-C, Rust, and memory safe language design generally.
What we shouldn’t do is reduce the discussion to claiming in a blog post that so-and-so “doesn’t like” such-and-such.
I think Fil-C is awesome and I very much like the style in which you present your work as well. (Hope I'm not being too emotional here!)
FWIW, I can see where TFA is coming from and I had the urge to write something "in defense of Rust" after listening to / reading some of your recent comments. I think your sister comment about the limits to Rust memory safety being real as much as the limits to Fil-C's performance and practicality is spot on, but the feeling (those emotions again) one gets from much of your commentary is that you downplay the latter quite a bit.
I think the real argument for Fil-C is that you aren't going to "rewrite in Rust" but you can often realistically recompile in Fil-C and pay the performance penalty; the fact that that Rust rewrite could also end up less safe is I think should be argued mainly from the AI angle where the only practical way is an LLM based rewrite but that would have a load of unsafe - way more than a "proper" rewrite - to be practical and the recent $100K Bun rewrite is a testament to that.
(I would love nothing more than for Fil-C to become as widely used as it practically can, since no other approach does nearly as much to secure C / C++ code and a lot of said code is out there; and security aside, it could prevent a lot of data corruption that most major C++ programs gift to their users.)
I do. Not for any technical reason, but because the Rust evangelists seem ready to burn me at the stake if i don't agree that memory safety is the one true god and Rust is it's prophet.
They hurt the language adoption more than they help it.
It is better in a sense that D is not C or C++. Technical innovation is memory safety with high level of compatibility with existing C and C++ in practice. It is compatible enough that you can run memory safe LibreOffice with Fil-C. D doesn't do that.
It's annoying how many comment sections online are now just Rust vs Fil-C / Zig flamewars, and the creators of those languages are deliberately fanning the flames.
I also feel there's a second dimension to the politics, where Rust is the "woke" language and C / Zig / Odin are now the "anti-woke" languages. At least, going by their most vocal online communities.
I'm sure offline there's still engineering decisions being made (I hope).
IIRC, ralfj was insinuating that Java was memory unsafe and arguing about it with experts on HN not too long ago. But I think he was coming off hot from the T-spec debacle so I understand it as him wanting to blow off steam.
The most toxic Rust leaders exited from the project a few years back, though some are flaming more freely than ever, e.g., Go barely deserves to exist, “SQlite is a terrible example” etc
They aren't. It's some random rust users vs the leader of the Zig project, leader of the Fil-c project, the head of community at Zig, etc. But they like to pretend there is an equivalence on the Rust side.
Andrew Kelley is a strong advocate for the LGBTQ and has railed against ICE in Minnesota.
I’ve only heard the woke/anti woke framing from Lunduke on the right, and a few Rust fanatics on the left. Both parties and their sympathizers want to fit the Holy Language war into the Culture war™
Repeat after me: Rust is not just memory safe C++!
Memory safety is a really big reason to use Rust, but it is very very far from the only reason. Even if Fil-C was magically zero-overhead (that is basically what CHERI is), I would still rather use Rust.
Rust has so many advantages over something like Fil-C it's hard to list them. I think Fil-C is a great project but it's only really relevant if you have no choice but to use C. If you can use Rust you also get:
* Compile time memory safety (Fil-C / CHERI are run-time).
* A modern functional-style type system (helps prevent logic bugs).
* Tree ownership (helps prevent logic bugs)
* Sane toolchain (helps prevent hair loss).
* Easy dependencies.
* Vastly more things checked at compile time.
Also I find it amusing how this post claimed it wasn't about Rust and then spent the whole time talking about it.
Rust is great. I don't think that really needs to be debated any more.
> And I also think it is totally fine to use Rust even if you could use a GC language like Go or Fil-C.
The problem with choosing Rust over a GC language like the above (or a good one, like OCaml) isn't that it's not "fine" to use Rust, but that manual memory management is an inefficient use of developer time. That's an issue for the developer, and one he inflicts on himself, not an issue for the end-user. Both Rust and (safe) GC languages provide memory safety, after all.
Rustafarians do seem to be worried by Fil-C and for good reason.
One of the top two issues with Rust is its cumbersome and overbearing syntax and anything which sidesteps that is automatically attractive to anyone which is worried about memory safety and doesn’t like to encode every little detail about in the type system. That’s the majority of programmers… think about it, that’s one of the big reasons people switched to GC languages which are the most popular languages.
The classic Rust approach to addressing criticism of the syntax was a mix of downplaying, gaslighting and “you’re holding it wrong”.
But if easy to use, reasonably ergonomic alternatives become available, I expect that most would prefer them to Rust.
Someone should fire up an AI and create Fil-Rust by connecting the Fil-C llvm backend to rustc, ending this flamewar. I guess it is well within the capabilities of a Fable class model, even if the driver doesn't have experience in compiler development.
> But Rust is unsafe, isn't it? It has unsafe after all! If you want to be that strict, or in other words, if you are a memory safety absolutist, that may well be true for you. I, and I hope most people, am more pragmatic than that.
So... Yes, Rust is less safe. Just that now the Rust apologist wants to back away from that and say that actually memory safety isn't actually the end all be all. C has a lot of security problems. Rust has less, at the cost of breaking the ecosystem. Fil-C has even less, and breaks the ecosystem. Why is the place Rust stops now suddenly good enough?
Like everything, it's about trade-offs. C is really unsafe. Rust is almost entirely safe, and breaks the ecosystem just a little bit. Fil-C is entirely safe, and breaks the ecosystem entirely.
You can just...not use it though? It's not comparable to trying to avoid it in C/C++ because there's literally just one keyword to never use and then you're good, compared to it being possible to silently introduce in any number of ways. I put a lint in my Cargo.toml to forbid unsafe code in my projects, and now I'm guaranteed not to write unsafe code.
To apply your logically consistently, you'd also have to throw out Go[1], Java[2], C#[3], and plenty of other languages. I guess you can define a logically consistent taxonomy of languages where Fil-C is the only "safe" language any everything else is unsafe, but then the burden is on you to prove that it's a useful way of thinking about them. Meanwhile, the way that people who consider Go and Java and Rust to be safe and C/C++ to be unsafe will continue to see actual real-world differences in outcomes when using a safe language compared to unsafe ones.
As an aside, it continues to be absolutely wild to me to see how most arguments in favor of C/C++ over Rust nowadays are based on framing things theoretically rather than practical ones. It was not all that long ago that the situation was reversed, with people claiming that Rust's benefits were all theoretical when most C/C++ code would have all of the UB found and removed over time. Now that Rust actually has been getting used for real-world stuff for a few years, and the number of vulnerabilities found in C/C++ code does not seem to be going down any time soon, any actual empirical evidence gets handwaved away. Maybe Fil-C will be able to provide as large benefits to running C safely in production like its proponents claim in the future, and if so, that will be a solid argument against the utility of Rust in a lot of situations, but if we're collectively going to shift the standard to discount pragmatism over theory in this debate, we might as well not try to hide it.
> To apply your logically consistently, you'd also have to throw out Go[1], Java[2], C#[3], and plenty of other languages
Yes? Is Fil-C not obviously safer than those?
> I guess you can define a logically consistent taxonomy of languages where Fil-C is the only "safe" language any everything else is unsafe, but then the burden is on you to prove that it's a useful way of thinking about them. Meanwhile, the way that people who consider Go and Java and Rust to be safe and C/C++ to be unsafe will continue to see actual real-world differences in outcomes when using a safe language compared to unsafe ones.
It seems like "can switch on unsafe whenever" vs "has no escape hatches" is an obvious line in the sand and a very useful distinction with real world implications.
> Maybe Fil-C will be able to provide as large benefits to running C safely in production like its proponents claim in the future
Why in the future? It exists and can be used right now. In fact, much of its appeal is being able to use existing code without needing to port to a new language; pragmatism seems like an argument in its favor.
> It seems like "can switch on unsafe whenever" vs "has no escape hatches" is an obvious line in the sand and a very useful distinction with real world implications.
Sure, although to me, the obvious distinction is that one of them gives you more flexibility (since neither Go nor Fil-C is at risk for someone accidentally writing unsafe code).
> Why in the future? It exists and can be used right now. In fact, much of its appeal is being able to use existing code without needing to port to a new language; pragmatism seems like an argument in its favor.
Because "can be used" is not a statement of present use, but potential future use. You seem to be missing my entire point about theory versus practice; plenty of things might be useful in theory but don't ever get widely used in practice (e.g. because they're not user friendly enough, or they require constraints that aren't acceptable to most users). I'm not making a claim about whether Fil-C has these issues, but making a claim that as yet, there does not seem to be widespread usage of it. If it's really so seamless to drop-in as a replacement for C that's strictly safer without downsides that make it unappealing to most people, I'd expect that to change. If that doesn't happen, I'd consider that evidence that it's not actually providing meaningful safety in the real world (in the https://xkcd.com/1312/ sense)
Bearing in mind that "there should be no room for a language above assembly and below Rust" (i.e. something like what C or C++ is to Python, Java, C#, etc.) was an intentional design decision for Rust which resulted in `unsafe`.
It seems very common for people to misunderstand that having strict accounting for unsafe is part of why Rust does what it does so well rather than just disallowing unsafe outright. I think it's easily confused because being able to choose "how much unsafe do I want to allow, and where do I want to allow it" allows the choice "I don't want any in my own code" to be easily enforced, and that's by far the most common amount needed, but it's certainly not the only useful one. Being able to handle the edge cases without having to give up the checks entirely is a huge improvement over most alternatives.
I don't immediately follow how Rust breaks the ecosystem. Rust has very good C FFI support and fully supports the default C ABI. It just requires unsafe bindings.
In practice, my Python packages broke when they dropped the C crypto support. I'm sure it was theoretically possible to avoid that, but they didn't. And that's ecosystem breakage. (And if the fix is that I the user must go manually install or worse compile extra things, that's definitely ecosystem breakage)
There are only three programs where memory safety matters: HTTP server, browsers, and operating systems. In practice, really just browsers and operating systems. Memory safety schemes that don't work for those systems are primarily cosplaying if their goal is safety.
In fact codecs in particular should be written in something much stricter but more special purpose than Rust, WUFFS (Wrangling Untrusted File Formats Safely)
WUFFS gives up generality - you can't write "Hello World" in WUFFS because it lacks both strings (for the "Hello, world" text) and I/O (for the printing it out). But you can write a codec, going from a block of bytes representing the encoded file to a block of bytes representing pixels, or PCM audio, or indeed uncompressed data [or vice versa] is easy.
But unlike Rust, all of the safety in WUFFS was checked during compilation. For example Rust emits bounds checks, arr[n] might panic at runtime if n is outside the bounds of arr, but WUFFS doesn't do that, it'll have proved mathematically that n is always in-bounds, if it can't prove that it rejects your code, make sure n is in bounds and try again.
Sometimes the end result is similar, you write code to check n at runtime, if it's a miss you report an error, WUFFS can see you met the criterion - basically the same as Rust. But often in a codec design you can just prove it's in bounds, if you implemented it correctly.
In these applications we want what's called "Functional safety" where what we care about is that the humans are kept safe. A Memory Safe language can be useful to help achieve this, which is why https://ferrocene.dev/ exists but it's also important to have business processes to assure that what the software is supposed to do will keep the humans safe, memory safety doesn't distinguish between "Ensure the human operator is in the containment zone when a cloud of toxic vapour is released" and "Ensure the human operator is NOT in the containment zone when it is released". But for that operator this difference is crucial.
I kinda wish we didn't push with the term "memory safe", and instead had a push with "correct". If your software isn't memory safe, it does not work correctly. We should aim to have software that works correctly.
The word "correct" already had a meaning. Memory safety is a subset of correctness in the same way that Rust can't statically prevent race conditions but it can prevent a subset of them called data races.
My biggest problem with the refusal to be memory safe is the fact that those problems end up becoming my problems when I am forced to use these applications and I have to think about how there might be a zero-click zero-day that uses an overflow in some random codec. Not as a software developer, but a regular person I want my application to be written in rust or at least use fil-c at bare minimum.
Now as a software developer I feel like this is even more important because I use libraries maintained by thousands of other developers that might also use applications that have these exploits which get their systems compromised pushing malware to thousands of other developers which end up compromising even more libraries.
I believe that memory safety should be the standard for software that thousands if not millions rely on and that it shouldn't be some political issue of X is better, Y is that, Z is something else.
But then again, social engineering is the primary source of malware spread so I don't know.
It seems to me that memory safety might be the difference between the software engineering and Software Engineering. As in, an actual Engineering discipline.
However, I should probably pipe down, as I would not call myself either one.
> memory safety might be the difference between the software engineering and Software Engineering. As in, an actual Engineering discipline.
I wouldn't go that far, what matters is the finished whole. Memory safety of the finished program is a critical factor and using a memory safe language makes it easier to achieve that goal.
However simply using a memory safe language doesn't make you a "Software Engineer" any more than using a certified I-beam makes someone a "Civil Engineer". What matters is that the finished structure/program meets the explicit and implicit requirements of safety, functionality, durability, cost, etc.
Not to mention that complex reliable systems are usually engineered out of much less reliable components.
Overall safety matters. Memory safety is just one factor. Log4Shell happened in Java, a GC language without pointer arithmetic.
With Fil-C and Rust the memory safety shouldn't even be discussed. It should be the bare minimum.
Presence of worse bugs won't make memory bugs disappear.
There are too many opensource projects that show that even with good engineering discipline humans are flawed creatures.
Memory safety is just little thing that makes sure that when you write code at 4am that it will not leak memory via trivial mistakes such as forgetting to free something, freeing something twice or passing a freed pointer. I believe AI agents shine here the most because the they do not get tired and are getting pretty damn predictable.
How many bugs in qmail though?
I don't think we should set our expectations based on an extreme outlier. qmail is special, and we can't expect most software to get to its level of security/safety.
Put another way: if you have to rely on programmer skill or attention to detail in order to guarantee something, that will always be a losing bet, on average. The existence of a tiny percentage of programmers that can clear that high bar does not make it a valid strategy.
Splitting programs into mutually untrusting modules is a plausible alternative to memory safety in programming languages.
Also in a program similar to qmail, memory safety alone is not sufficient, a lot of bugs in sendmail were related to complexity issues.
If only there was an OS design architecture that would follow such approach from the ground up....
Why can't we write everything like qmail?
It had one bad cve it seems, but that's exactly what I mean. It only takes one mistake, of course you can learn and never make those mistakes again, however, that is an unrealistic expectation in software that receives hundreds of feature updates a year especially when it comes to core applications as basic as communication when it wants to support image previews, reels and whatnot.
There will always be one, so "it only takes one" is meaningless and invalid. That leaves less is better than more, and any form of less is as good as any other form of less.
Some Rust programs also had RCE CVEs.
As a user you should be far more worried about running up-to-date software and supply chain risks rather than zero-days related to memory safety.
You wouldn't have to worry so much about running up to date software if memory safety were pervasive.
This is wrong as there are many other safety issues to worry about.
But memory safety is one of the big ones.
Most people I know that had security incidents did not have this because of memory safety issues. But it does not matter, even without memory safety issues out of the picture, you would need to update your software and be wary of supply chain attacks.
(Actually, I can't remember a single incident where somebody I knew was directly affected by a memory safety issue)
If as a user you're willing to pay these library/application owners and premium to do so, by all means; this is a reasonable demand. But short of a massive campaign to educate and change minds, I can't see the average user caring enough.
My biggest gripe honestly is businesses rather than any particular opensource project, opensource projects can often get away with these issues getting caught by the many eyes looking at them before they ever make it to mainline.
You will never change my mind.
So you have a dogmatic belief, not based on evidence then.
> I'm hoping, though, that most of the Rust devs, who say they care about memory safety, genuinely care about making software safer, and not just criticizing languages that compete with Rust.
Well I feel most of the Rust devs are there for saying shits about other languages to prove their own dumbassness.
This is perhaps a different take, but one reason I love Zig and C is because I've learned how to reason in pointers. I've worked with Rust in the past on a ~12,000 LOC side project, so I was all in with tree ownership and XOR mutability. But it turns out there's all sorts of delightful data structures that you can only express with unsafe in Rust. Intrusive doubly linked lists are the coolest thing ever. The fact that I can have a LRU cache that changes what's at the front by shuffling some pointers, while still keeping stable addresses so the hash map stays stable? That's freaking cool. Atomic operations with pointers for linked lists is really slick for implementing an allocator's free list between threads. Being able to walk a live heap using a breadth first search by just... following pointers, made the elegance of Dijkstra's algorithm come alive.
Now, maybe I'm only running into these algorithms because these are the problems I'm running into, but in the Rust community I consistently got the message that linked lists were a legacy data structure. In some cases they are, but when they do apply they do so brilliantly.
I know working in Zig leaves plenty of room for memory unsafety. Perhaps that's a bad thing. But I'm implementing an interpreter, and these algorithms are essential for it to run fast. I really do need to be aware of where every allocation happens, and what instructions the computer is running. So for now I stick with Zig, even though I know I'm opening myself up to memory exploits.
Given you like pointers so much a dislike for Rust would make sense if Rust didn't have pointers, but it does and IMO as somebody who spent years getting paid to write in C, Rust's pointers are better.
I don't know if Zig has made any clear decisions about this (chime in any Zig experts) but in C [and C++] the pointers are crap because they're "zapped" when the thing pointed to is gone. Now of course in reality your compiler won't overwrite your pointer just because the standard says it could, but because the compiler knows it would be allowed to do that, it can optimise pointer tricks in surprising ways. To work around this, C and C++ provide pointer-sized integers and exhort you to do any tricks with those instead because there's no zap.
In Rust that's unnecessary, the pointers are just pointers, if the thing pointed at is invalidated, you mustn't dereference that pointer because it's not pointing at anything now, but you can still for example XOR it against another pointer. Clever pointer tricks are thus your responsibility as programmer, and you can just do them with pointers rather than needing this pointer-sized-integer workaround.
Linked lists are cool but they are terrible for cache performance, so their apparently elegant performance characteristics are often illusory. And hybrid data structures which get you the best of both worlds are quite complicated to implement. I think that's why there's a general pressure against using them unless you have a very specific reason (and ideally some measurements) to demonstrate that they are a good option.
(part of this I think is some C teaching which tended to overemphasize linked lists because they are useful for teaching the concept of pointers, which gave a lot of people learning C the impression that it should be the default way to represent a list of objects, when it's far more the exception than the rule)
Yeah, I'd blame mainly 2 factors for the continued undue influence of linked lists
1. CS professors teach them. The Programme Lead for our CS course still teaches linked lists as the first data structure in the DSA course. Does this type exist? Sure. Is it a good idea? Almost never. But you wouldn't think so from its prominence in the course materials.
2. The Linux kernel uses a LOT of linked lists. Multi-core atomic operations on mutable data, a nightmare you will probably avoid in your software is necessary for the OS kernel and so it has linked lists. Linux is very famous, but your software is almost certainly not an operating system kernel.
> but your software is almost certainly not an operating system kernel.
In general yes, but right now I'm contributing to folk.computer, which is a multithreaded task scheduler/central DB (among other things not relevant to the topic at hand). The biggest use of atomics is during statement insertion and removal, by using RCU operations in the central trie.
One of our current performance drags is the interpreter we use is not threadsafe, so we have to serialize and deserialize objects as they move between threads. This contributes to about 30% of Folk's CPU usage. So I'm currently working on making a threadsafe interpreter by porting the Tcl interpreter we use (Jimtcl), and I'm learning all the fun things around atomic ordering and threaded data structures. So I'm definitely the audience for these data structures.
Traversing a linked list is terrible for performance. But in cases like the cache you are only poping from the front, appending to the back or removing which are all quite fast and require little pointer chashing.
Cache cost of individual allocations can be an issue but that can also be mitigated with a good allocator and even without special care can be quite performant in many applications.
I agree the linked lists shouldn't be the first structure you reach to. But there are situations where they can perform very well.
Even Lisp got all major data structures relativity early on, building only on lists is mostly used for prototyping.
Well, there is another safe option for flexible pointers if you can compile as C++ [1]. In C++ you can have non-owning "smart" pointers with run-time-checked lifetimes [2][3]. Importantly, there is no run-time overhead to dereference them. (That is for the "never-null" versions, otherwise there's a null check.) Assignment has additional run-time cost, but pointer assignments are generally much less prevalent in performance-sensitive inner loops than dereferences, right?
And you can statically verify [4] your raw pointers, so that you only need to use the run-time-checked pointers for the more exotic lifetime relationships.
(That said, for the situations where Fil-C acceptably solves your problem, it's probably the more practical, complete and well-supported solution. And since not many seem to be explicitly mentioning it, the recent Fil-C demonstration of memory-safe linux userspace is rather impressive, right? I've heard that IT security is a $100B industry. I'm guessing an inappropriately low proportion of those resources are being invested in Fil-C. :)
[1] https://github.com/duneroadrunner/SaferCPlusPlus-AutoTransla...
[2] https://github.com/duneroadrunner/SaferCPlusPlus#norad-point...
[3] https://github.com/duneroadrunner/SaferCPlusPlus#tnoradproxy...
[4] https://github.com/duneroadrunner/scpptool
Interesting, this is pretty cool! I'm guessing it doesn't work with pointer to int casts though? I'm using NaN packing, so I necessarily have to cast it to an integer.
EDIT: though I'm considering switching to a tag + 64 bit union, because of 54 bit addressing and pointer tagging. It makes it tricky to get my data layout right though, since it makes all of the structures larger.
> But it turns out there's all sorts of delightful data structures that you can only express with unsafe in Rust. Intrusive doubly linked lists are the coolest thing ever.
I don't mean this to be a gotcha, but I think it's important to say that we can have these in rust too! The only difference is that the first thing we have to talk about is you the user can go about using an intrusive collection correctly. I love the cordyceps crate for this. If you want to be able to use your type as a linked list node, you must implement a Linked trait, the documentation of which clearly explains how to use it safely: https://docs.rs/cordyceps/latest/cordyceps/trait.Linked.html
I think this crate description encapsulates what's difficult about unsafe rust, which is how unergonomic pointers are. Like why do I need to use `addr_of_mut!`? I'm sure there's a good reason, as Rust tends to think these problems through, but it's very unintuitive compared to returning `&mut`. I feel like I'm juggling way more concepts, which to be fair helps with safety, but it can obscure the algorithm itself.
Does cordyceps have a derive macro? I can imagine that helps a lot with correct implemention, though when it comes to linked lists I can see people wanting to do it themselves.
> Like why do I need to use `addr_of_mut!`?
As of Rust 1.82.0 [0] you no longer need to!
> I'm sure there's a good reason, as Rust tends to think these problems through, but it's very unintuitive compared to returning `&mut`.
The addr_of_mut docs [1] give a pretty decent explanation of its reason for existence; in short, it lets you get a pointer to something without needing to create potentially-invalid intermediate references. It (and &raw) probably aren't going to be needed if all you need is a &mut, though.
> Does cordyceps have a derive macro?
Doesn't appear to from a quick glance, and given what's in the safety section of the docs [2] it'd probably need to be marked unsafe [3]. Unsafe attributes are new to Rust 2024, though, so that might be a bit new.
[0]: https://blog.rust-lang.org/2024/10/17/Rust-1.82.0/#native-sy...
[1]: https://doc.rust-lang.org/std/ptr/macro.addr_of_mut.html
[2]: https://docs.rs/cordyceps/latest/cordyceps/trait.Linked.html...
[3]: https://doc.rust-lang.org/edition-guide/rust-2024/unsafe-att...
> > Like why do I need to use `addr_of_mut!`?
> As of Rust 1.82.0 [0] you no longer need to!
That's true but misleading. I'm pretty sure the question was why a normal reference is bad. You don't need `addr_of_mut!()`, but you do need `&raw mut`.
So how do I take a shared reference to a variable named "raw"?
Complicating the grammar for no good reason. This should have stayed a macro.
Rust allows you to use a literal iirc like r#raw, which helps with migration. This isn't too different from Zig's @"var" for example, which I'm glad modern languages have an escape hatch for naming things that don't parse normally.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
`raw` is a contextual keyword.
Interesting. I stopped working with Rust about two years ago, so it looks like there's been a lot more polish in these areas.
> what's difficult about unsafe rust, which is how unergonomic pointers are. Like why do I need to use `addr_of_mut!`?
Things are slowly getting better here! '&raw'/'&raw mut' reference operators were stabilized a couple years ago.
Another ergonomic improvement in the pipeline is a better way to access fields behind pointers, something like C's -> operator. This is taking some time to design because there are things other than raw pointers that would benefit from a generalized field projection mechanism, like Pin, NonNull, and (potentially user-defined) smart pointer types.
> Like why do I need to use `addr_of_mut!`? I'm sure there's a good reason, as Rust tends to think these problems through, but it's very unintuitive compared to returning `&mut`.
That documentation appears to be out of date. The `addr_of_mut` macro was elevated to a first-class language feature, `&raw mut` (there's also a corresponding `&raw` operator). These two operators differ from the usual `&mut` and `&` in that they create raw pointers rather than references; prior to the introduction of this feature (or the aforementioned macros that served as precursors) to create a raw pointer you might need to have done a cast like `&foo as *const` in order to create a raw pointer by casting from a reference, but this could have safety consequences if the temporary reference was to an invalid object. Therefore `&raw` and `&raw mut` were introduced to create raw pointers directly without introducing an intermediate reference, which was arguably the most subtle footgun in unsafe Rust for a few years, and it's nice that it's now addressed.
I absolutely agree learning C is a good thing! In fact, there are two camps of Rust people: one that argue that you should not learn C before learning Rust, and one that argues that you should.
Also, while such data structures/algorithms are rare, and more importantly, they can be written once and used many times, someone still needs to write them!
Here is some online content on this side of Rust:
- Learn Rust the Dangerous Way - https://cliffle.com/p/dangerust/
- Learn Rust With Entirely Too Many Linked Lists - https://rust-unofficial.github.io/too-many-lists/
- The Rustonomicon - https://doc.rust-lang.org/nomicon/
Thank you! I've seen the latter two mentioned in the past, but I never got very far into the books before getting confused with all the moving pieces. I think now that I've worked a lot in languages that put structs and pointers in the forefront, I'd understand it a lot better. Perhaps I'll have to go back and give them a proper read this time...
> The fact that I can have a LRU cache that changes what's at the front by shuffling some pointers, while still keeping stable addresses so the hash map stays stable? That's freaking cool. Atomic operations with pointers for linked lists is really slick for implementing an allocator's free list between threads. Being able to walk a live heap using a breadth first search by just... following pointers, made the elegance of Dijkstra's algorithm come alive.
I feel like these kinds of things should be transformations that the compiler can use to turn Provably Safe code into performant code, i.e. you write the safe code and, once the borrow checker is happy, the compiler does its magic. I have no idea how to codify any of it into a compiler pass, or whether it's actually possible...
There's some interesting work going on with this with Metamath Zero[0], where he's working on verifying everything down to the compiler, to make sure the compiler always emits correct code. His language, Metamath C, also allows for refinement types, which means you can prove certain properties. It's still in its infancy, but I've been watching it from the sidelines for a bit now.
Though in terms of practicality, Graydon Hoare has mentioned in the past that he isn't a big fan of complex systems, and so it's better to have a simple type system that covers 95% of the cases, and an escape hatch when it doesn't. I think that's still a fair assessment, but I've fallen down the proof rabbit hole so I am curious to see how far it could be pushed.
[0] https://github.com/digama0/mm0
Sadly due to cache friendliness, current conventional wisdom is that linked lists are so slow that a dynamic array of pointers is almost always faster even in cases when you'd think linked lists would be faster, such as frequent insertions in the middle.
You are right on the money! I have been saying this for a while.
Apart from the utility, it is also a lot of fun. So it would be a pity if people are told that these things are legacy and they should not try to use/implement them..
Given your performance concerns, I'm imagining you're unlikely to run Zig-Fil in practice, due to the GC overhead?
Yeah, probably not. I wouldn't mind fuzzing it with Zig-Fil, but the interpreter (Zicl) is embedded in a larger C project that uses a lot of dynamic linking, so chances of using it in production is pretty much zero. I do have a lot of asserts that only have a small overhead, so I'll probably use ReleaseSafe most of the time, since it does catch a lot of the issues.
See now this is why I don't quite get everyone jumping on the rust bandwagon.
This is a limitation of the rust model. You could have a language where intrusive linked lists are provably safe.
That's not to say rust doesn't work, but it's the first language I'm aware of that's attempted compile time memory safety, and no body seems to be talking about how to do it better.
Personally, I see these language fights as being a bit pointless. They are trying to optimize at the wrong layer. Unless one is working with a dependently typed language, which requires a proof assistant to discharge type checks, then it's all just a question of where you make the trade-off. Don't care about memory leaks or deadlocks as part of your soundness guarantees, but can't have GC? Use Rust. Okay with runtime overhead to verify checks? Consider fil extensions.
If you want to go deeper, skip the language wars entirely. Tooling does what these languages can't do. I have had great success model checking C with CBMC. Not only does this prevent memory safety issues (including memory leaks), but this also prevents deadlocks. Bonus: you can write user contracts and invariants to verify that every execution path of a function fulfills these contracts and invariants.
Kani comes close with Rust. It doesn't yet have decent concurrency support, but there is nothing that prevents this from being added in the future. The ability to write custom contracts and enforce custom invariants more than makes up for its lack of concurrency support. Similar technology could be used or adapted for Zig or C++.
Use whichever language you like. Just, please look into model checking it. The technology scales just fine, once you get over the learning curve and learn how to use compositional verification.
Pizlo is not a memory safety absolutist. His rhetoric towards rust is a tactic specifically designed to draw more attention to him and his project. It is amplified by people who already had a bone to pick with rust and take joy in giving rust folk "a taste of their own medicine," so to speak. Articles like this are taking the bait.
What a weird take.
I think the limits of Rust’s memory safety are interesting to discuss, as are the limits of Fil-C’s perf and practicality.
It’s best to discuss these things rationally, rather than accusing folks of trying to draw attention
I don't think it's too weird of a take, we do things for many reasons. I get that it feels bad to have that directed at you as an accusation. It's rational to discuss the intent behind actions, but I think the parent is hinting at "so we shouldn't take what Pizlo says seriously", which isn't a good conclusion.
This made me think and reflect, thank you.
Not so much about language design as how easy it is to take a defensive position on something we are comfortable with when it's challanged. This prohibits growth.
When someone suggests you have been doing something the wrong way or has a new idea, it's usually better to hear them out and take what knowledge you can from them, even if they weren't actually trying to help you.
Anyways, thanks again
Trying to figure out Fil-C's "inviscaps".[1]
When you allocate space with Fil-C's "malloc", some additional bounds checking data precedes the space the program gets to use. Pointers are "fat pointers", with a pointer to the beginning of the buffer and a pointer to someplace within the buffer, allowing ordinary C pointer manipulation.
There's much new verbiage around this. But it's roughly the same idea as GCC "fat pointers".[2][3] So it's not a new idea. It's one that's been tried several times, but never caught on.
There's a performance penalty. Especially if the compiler can't hoist the checks out of loops.
[1] https://fil-c.org/invisicaps
[2] https://williambader.com/bounds/example.html
[3] https://www.doc.ic.ac.uk/~awl03/projects/miro/MIRO.pdf
Invisicaps are not exactly fat pointers.
Fat pointers show up inline in memory, which has a bunch of problems:
- sizeof(void*) changes
- either you let the bounds get corrupted by bad casts, unions, and other issues, or you impose restrictions that prevent unions from working compatibly, or you end up supporting unions by having issues with races, or you need special hardware. Invisicaps sidestep all of those issues
The "flight pointer" looks an awful lot like a fat pointer. See the diagram at "The intuition of inviscaps" in [1]. A "flight pointer" has a pointer to the beginning of the buffer, which they call the "lower bound ptr", and a pointer to someplace within the buffer, which they call the "integer ptr". The pointer to the beginning of the buffer lets the checker find the size of the buffer, which is stored preceding the buffer. The compiler has to arrange things so that the lower bound ptr is carried around wherever a "flight pointer" goes. This is supposed to be invisible to the programmer.
[1] https://fil-c.org/invisicaps
other implementations of fat pointers (that I’m aware of) have no distinction between flight and rest; they store what I call flight pointers in memory literally.
The closest technique to invisicaps is softbound, but that has issues that invisicaps resolve (better story for races, more comprehensive safety for all of the C and C++ languages, no need for large virtual memory reservations, and lock freedom)
> The thing is, programs that can be also written in GC powered languages, often don't need unsafe at all
That's true, though it's worth noting that they do still need unsafe for the FFI. Often this is where the memory safety issues arise in GC languages. So no language is truly "memory safe".
Which is why in some OS written in memory safe systems languages, like Burroughs, still being sold by Unisys as ClearPath MCP, any use of unsafe code blocks taints the binary.
Execution is only allowed after the OS admin adds the executable to a specific process white list.
This is similar to how some managed runtimes work, and the Java ecosystem is moving forwards it.
Currently it only triggers a warning, however in the future JNI and Panama will be disabled by default, and like with ClearPath MCP, it is up to the person installing the application to enable the unsafe layer explicitly.
Another example is the capabilities approach on WASM runtimes, here again the one executing the platform takes ownership on enabling unsafe behaviours.
That's probably because the OS itself didn't have great defenses, right? We have permissioned virtual memory now.
It surely did, MMU were already a thing.
The customers that still buy such systems, care about security as the feature above anything else.
As you can see, the marketing is all about security.
https://www.unisys.com/product-info-sheet/ecs/clearpath-mast...
I don't think you need to be an absolutist or only use memory safe languages but it's very obvious that we need to do a whole lot better than we actually do. Rewriting in Rust or any other language is one way to do that, but not a perfect one. I'll accept C++ when most C++ software has 1 in 50 chance of an RCE and not just a 1 in 50 chance of a known RCE. I think we can get there but we are not currently there.
Coreutils has a good track record. Ffmpeg doesn't. They should have rewritten ffmpeg in Rust, not coreutils.
There are other approaches though like formal verification.
ffmpeg is actually asm and for a good reason - it has to go fucking fast or else media playback will take too much resources
Great then let's prove that ASM is correct.
the reference C code is just as bad.
Which is actually possible, unfortunately the industry never cared that much about strong typed assembly.
See Verve OS from Microsoft Research, TAL and the origins of the Dafny language.
It's smallish snippets that implement self-contained algorithms, which should make it well within reach of direct formal proof of correctness.
If you go by this metric (has a RCE and not known to have RCE), I'm pretty sure the actual statistics are more like 1 in 2, at least. And probably more than one RCE.
>They should have rewritten ffmpeg in Rust, not coreutils.
Wait for an AI company to promote their new model by porting the entire ffmpeg to Rust (half ironically).
Didn’t the primary maintainer already signal burnout or frustration or something like that? If an AI company actually did port ffmpeg *successfully* (not like claude CC), the dude might get the release he might have been yearning for
I think the "Fil-C is safer than Rust" thing is an understandable reaction to 10 years of Rust evangelists telling people they have to use Rust otherwise they're stupid and wrong.
The reasonable reaction is to ignore the vocal minority.
It’s tiresome when other language communities start engaging in a battle with a vocal minority of another community.
Every language has its annoying maximalist pushers. The play is to ignore them and do what decisions are best for the language, not to let yourself get dragged down into petty language wars where nobody wins.
We can learn a lot about memory safety from Rust. But if we can achieve the same dream of Fil-C for other more common languages that need it, Rust's popularity will likely wane.
I doubt it. I am a big fan of Rust's memory safety, but it's not just memory safety that draws me to Rust. Having a strong, featureful type system is also a big plus for me, and even if Fil-C could someday magically compile any program or library without modification, and its performance trade offs could be mitigated, I'd still write Rust, because C is miserable to write. (And I say this as a 25+ year C programmer.)
Fil-C has a large performance impact though, and it'll always be reasonably significant. Rusts popularity has always been the combo of safety and performance, otherwise you might as well just use C#
> Rusts popularity has always been the combo of safety and performance, otherwise you might as well just use C#
Combined with the ability to compile to a single native binary, having a solid package management solution, a fairly advanced type system, and a solid selection of nice language features. Usually you need to give up at least one of those.
Had some headaches with nuget, and C# type system isn't as advanced as Rust or TypeScript, even less so in comparison to Haskel (still solid notheless), but it really is a solid choice. Specially in comparison to Java.
> Combined with the ability to compile to a single native binary, having a solid package management solution, a fairly advanced type system, and a solid selection of nice language features
Good to see a praise for C#.
Happy to see it's finally getting union types in 2026
More people should use C#, to be honest.
Good article. Not much to add to it, other than I think more people should look at modal type systems like found in Scala 3 and OxCaml. If you want safe arena allocation they are a lot more ergonomic than Rust's approach.
I don't the fanaticism around memory safe languages. We've had memory safe languages for a very long time. Algol-60, the granddaddy of many modern languages, had it sixty-five years ago. Algol-60 also had other safety features that modern languages don't have - for example integer overflow safety.
The real issue is that we came to accept unsafe languages and are taking a really long time to put such features back.
The real issue is effort. We accept ffmpeg because nobody tried to rewrite ffmpeg in ALGOL-60. We're happy to go through contortions to make ffmpeg or sqlite usable from Java because it's useful and not written in Java and nobody wants to rewrite it in Java.
The article points out what I think is the crux of the discussion: With Fil-C they become crashes. Errors manifest at runtime, not at compile time.
I realized some time ago when I talk to people about rust, I don't really mention memory / thread safety in a security context, I mention them in a reliability context. Compile-time checks mean I basically never spend time debugging runtime crashes, and generally can count on the compiler to catch memory and thread safety issues before the program ever runs, and this saves me time and aggravation in production. I care about security, but I care more about reliability, and rust compile-time checks mean my software is reliable in a way that runtime-checked languages just aren't.
This applies to lots of languages. Every time I hit a runtime crash in python, or ruby, or js I die a little inside. Even though they are memory-safe because they're garbage collected, I still waste time debugging runtime errors.
Fil-C is basically an alternate ABI and libc runtime. Otherwise it is not tied to C and I see no reason it couldn't be targeted by Rust or Zig.
I think Fil-C ABI implemented by all of C, Rust, and Zig is the future. But that would need some sort of stability for interoperation, and stabilizing ABI takes time: Rust still doesn't have one (while Swift spent enormous amount of effort to have one). Experimental implementation is probably still worth doing.
It currently is at least associated with C in the sense that it takes C code as an input. But yes, I would be very happy to see its approach applied to more languages.
Yes
I was not aware of the antagonism from the Zig / Fil-C people to Rust, but it makes absolutely no sense.
Like, of course you can prevent all memory safety errors by using a garbage collector. That has been known since the 90s. In fact, there was a good two decades after Java released where all the research on memory safety just stopped, because the standard answer became "use a GC". If you couldn't use GC, you were stuck with languages made prior to Java - which in practice meant just C - or the one language everyone tried to staple every new programming paradigm onto, C++.
In fact, part of why C++ became such an untameable beast of a language is because it became load-bearing for non-GC projects. Anything not in the ISO C++ standard was, in practice, something you just couldn't do in native code. Oh, of course you can't introspect structs in a compiled language, of course macros are useless and unhygenic, of course templates have to be monomorphized and bloat your binary size. And so the pressure for C++ to be an all-singing, all-dancing, all-dressed language grew.
Rust's big story is memory safety, but the more Rust I wrote, the more I realized that the memory safety is only part of the picture. Rust has a very nicely curated selection of features that allows the language to remain understandable despite the sophistication of the compiler. Memory safety is a selling point, sure, but it is also the lubricant that makes working with those features pleasant.
If you want a real complaint about Rust, it's that some features are oversimplified in ways that make certain scenarios harder and make some features way more "magic" than they should be. Have you ever tried writing Futures code without making use of the async keyword? It's nearly impossible, for several reasons; the main being that Rust's type systems cannot express self-referential borrows. This also makes returning a reference to something in an Rc or RefCell you own more difficult[0]. And there are numerous other states memory can be in that are hidden from Rust's type system. Rust can't even represent a real destructor fn. Dropping a value multiple times, or using it after it's been dropped, is explicitly forbidden; but Drop impls still can't take values out of themselves because Rust doesn't have an "owned reference" - i.e. memory you can take values from but can't deallocate.
But none of this compromises memory safety - it just makes certain things harder than they should be.
[0] Strictly speaking, there's an owning_ref crate that manages this; stdlib is also working on a "mapped mutex guard" type that would do the same thing without a dependency.
> I was not aware of the antagonism from the Zig .. people to Rust, but it makes absolutely no sense.
Lol, have you ever watch Andrew Kelley speak? I've only seen 3 or 4 talks he's given, but he frequently makes not-so-subtle digs towards Rust. Zig's home page prominently displays "Focus on debugging your application rather than debugging your programming language knowledge" which sounds horrible to me, but is clearly an anti-rust statement. Competition is good, and Rust and Zig cater to different people, but when the primary personality behind Zig is leads the way, it shouldn't be a surprise that some tribalism-influenced followers lean hard into it.
“Focus on debugging your application rather than debugging your programming language knowledge” to me sounds like it is directed primarily at C and C++., it’s a much more direct translation.
It could also apply to Rust though. It's on the complexity scale of C++, not C.
> "Focus on debugging your application rather than debugging your programming language knowledge"
This sounds like it's made by someone who fears discovering they don't know something more than discovering they made a mistake.
I am the reverse: I would prefer to debug my programming language knowledge over debugging my application. I hate debugging applications specifically because making a mistake is SO much worse to me than discovering there is a fact I didn't know yet.
And, of course, debugging your application is discovering there are unknown unknowns in deployment, the worst of the quadrant of knowns and unknowns.
Debugging PL knowledge: When you do 4+4 on a Tuesday at an even numbered address it's 9 - is all your software safe against this?
The thing that has changed is that there are spaces where security is being taken more seriously, and C's memory unsafety became a deal breaker there. I do think that providing a mechanism to run existing C software that isn't performance sensitive in a way that mitigates its limitations is very worthwhile.
I think the "static analysis" and the "runtime checks" approaches are complementary, not in opposition, making any noise around having to choose one or the other moot.
One would think that by now it would also be on WG14 roadmap for language evolution.
Same in regards to WG21, while more active in improving safety, there are plenty of politics between the profiles approach faction and everyone else.
> Like, of course you can prevent all memory safety errors by using a garbage collector. That has been known since the 90s.
No, it's been known since GC was invented.
> In fact, there was a good two decades after Java released where all the research on memory safety just stopped, because the standard answer became "use a GC".
Having done all of my research on memory safety after Java was released, I find this statement a little exaggerated... (e.g., https://barnowl.org/research/pubs/98-pldi-regions.pdf, https://barnowl.org/research/pubs/07-hotos-linux.pdf)
Yeah, my understanding (which is not from first-hand experience) is that a huge part of the origin of Rust was that there was a lot of research in memory safety techniques that had been going mostly unused by mainstream languages, so the idea was "Why not try to make a language using some of it?"
AT&T was already doing that as language to replace C, Cyclone.
AT&T even had a full OS, where C only had minor role, the microkernel and Dis VM/JIT, with the whole userpace implemented in Limbo, designed by the creators of UNIX and C.
Good reminder that memory-safe alternatives to C aren't new. Rust may have won more because of ecosystem and timing than because it was the first to address the problem.
All the safety improvements of 21st century wannabe C replacements, were already available in NEWP(1961), PL/I (1964), Mesa (1976), Modula-2 (1978), PL.8 (1982), Ada (1983), Object Pascal (1986), among many other lesser known ones.
However none of them had an OS available in source code for the symbolic price to send tapes around to universities.
Additionally, the remark that the UNIX and C authors themselves did not stop there, and their latest mark on the computing world was the Go programming language, after Limbo, and the acknowledgement that designing Alef without a GC was a mistake.
Probably they would not had to reboot Plan 9 OS design into C.
*nod* They're from before I started bookmarking rigorously, but in the early days of Rust, there was a sentiment in the blog posts from the people developing it that the point of Rust was "to give good ideas a second chance" and that Rust was intentionally boring and un-innovative.
> you can prevent all memory safety errors by using a garbage collector
Garbage collection and a higher-level language that controls allocations handles 2/3 of the memory-safety problem space (using memory you shouldn't via use-after-free, or using memory you shouldn't before allocation/via arbitrary address access), but the other 1/3 isn't addressed by GC: out-of-bounds access on properly-allocated structures.
GC-or-not doesn't stop a pointerful language from addressing memory off the end of an array, and the best techniques we have to deal with that today are imperfect (but still pretty good!) compiler inference for BCE, memory protection to limit the scope of possible overreach, or slowing down runtime by inserting checks.
The rest of your points are well-taken; just pointing out that GC isn't the solution to all causes of memory-unsafety, just 2/3 of 'em.
You forgot the other three thirds GC doesn't handle: in-bounds writes to memory currently in use by another processor. One of the basic ideas behind Rust's memory safety story is that eliminating race conditions necessarily requires a good memory safety story, especially regarding temporal memory safety.
...of course, the managed code languages had an answer to this: put a lock on every managed object you allocate.
But they didn't actually make you acquire the lock - it only comes into play if you declare a method to be synchronized or if you acquire the lock externally. Which is a problem because most of these languages ALSO are designed to act as a security boundary for untrusted code - something Rust doesn't even offer! Which means even though the language doesn't enforce race-freedom, it has to enforce enough of it anyway to keep racy code from corrupting the language heap (which is now security-sensitive). No other language (not even Rust) does this - if you write a race in unsafe Rust, it is actually UB, but in Java you actually still have some guarantees about what your program will do.
Actually, let me throw a bone to the Zig people: Unsafe Rust actually imposes more UB than C does, and it is more difficult to write sound unsafe Rust. If you're writing FFI code[0], you're probably fine. But if you're writing new memory abstractions, you're probably going to run into Rust's requirement that all mutable references be exclusive. If this ever fails, your program is already in UB and all bets are off. Morally speaking, Rust sprinkles the equivalent of C's restrict keyword over every &mut in your program. In fact, this UB is so strict that the Rustc people have had to turn this on and off multiple times in the past because LLVM would miscompile sound / safe Rust code if it was told about the aliasing restriction.
In practice, however, this doesn't really matter. Rust has all sorts of soundness holes nobody would ever trigger by accident. There's a long standing trait-handling bug that lets you confuse memory types in safe Rust; and any OS that exposes process memory as a writable file lets you do the same thing. In Java, at least the former would be a CVSS 10.0 security vulnerability, but it doesn't matter in Rust, because safe Rust is not a security boundary. It's a set of tools to keep you from shooting yourself in the foot. And, in practice, Rust does a pretty good job of that.
[0] Or more generally, unsafe code where you have two unsafe pieces that you have to guarantee are used together. For example, if you have a C-style callback API that takes a function pointer and a data pointer, and then calls the function with the data, you have to use Box::into_raw() and Box::from_raw() to maintain ownership over the data. from_raw is unsafe, but all the obvious ways of using it are sound.
> One of the basic ideas behind Rust's memory safety story is that eliminating race conditions
Rust does not prevent race conditions. You're getting confused with data races.
However GC's solution to data races (make every load/store act as very relaxed atomic instructions) makes race conditions much easier to write.
Rust does prevent race conditions.
It prevents "user A kicked B" and "user B revoked moderator permission from A" from arriving in a queue near the same moment?
Rust helps prevent some race conditions, but an unqualified "prevents race conditions" sounds like it eliminates them, hence why GP says it does not.
Fearless concurrency only works as advertised when those data structures aren't exposed via the MMU to other processes, or are handles to resources like files or database connections, where each thread can do whatever they want without type system checks.
All relevant scenarios when the goal is systems programming.
> Strictly speaking, there's an owning_ref crate
owning_ref is unsound and shouldn't be used. self_cell/yoke/ouroboros are much better alternatives, ableit more complex
> Like, of course you can prevent all memory safety errors by using a garbage collector. That has been known since the 90s.
Since the 60s actually, starting with Lisp, Scheme, CLU, Cedar, Smalltalk, Cedar,...
The main problem of Fil-C or similar solutions is not that they provide absolute safety with no escape hatch (unlike languages with unsafe keyword). The problem is that they provide an excuse to keep using terrible programming languages like C and C++ allowing memory safety issues in the first place.
Yeah, but who's going to rewrite LLVM in Rust, which rustc relies on?
Yes there is Cranelift, and?
In case of such projects like LLVM C++ usage can be tolerated. But for new code or smaller codebases a better alternative should be considered.
Also Fil-C can't be used for LLVM anyway, since performance and memory consumption overhead is way too much. Nobody wants clang/rustc working 4x slower and consuming 2x more memory.
Yeah, unfortunately there are plenty of such projects, without alternatives.
GCC, GNOME, KDE, Linux kernel, BSD variants, CUDA, RocM, OpenCL Vulkan, DirectX, Metal, GNM(X), OpenMP, OpenACC, NVN,....
Hence why somehow fixing C and C++ is also quite relevant for the upcoming decades, assuming humans still matter on the planet.
Now it is going to be ARM MTE, SPARC ADI, CHERI, Fil-C, or WG14 and WG21 actually getting their act together, remains to be seen, as it is subject to governments and industry pressure, and we not destroying civilisation.
> GCC
Isn't needed if we rewriting anything in Rust anyway.
> GNOME, KDE
They aren't that huge. Huge is the overall codebase of applications using them. It's relatively easy to create a native Rust GUI framework and rewrite applications needed such a framework one by one.
> Linux kernel
It's a good opportunity to rewrite it. Not only because modern languages like Rust are better than C, but because such a rewrite allows getting rid of old stuff present only for historical reasons.
> BSD variants
Aren't needed if we writing a new kernel from scratch.
> CUDA, OpenCL
Not a huge problem. There are dozens of GPU-related languages. Adding one more isn't that problematic.
> Vulkan, DirectX, Metal
They are language-agnostic (but still unsafe). One can create an implementation in something other than C.
The fallacy starts that Rust depends on GCC and LLVM, and someone will keep adding code to them.
Likewise with all the industry standards based on C and C++.
Looking forward to see how COSMIC ever manages to gain adoption beyond System 76 computers.
Once again, people are grappling with an axiomatic definition of "memory safety" and avoiding the fact that it's a term of art with a very specific meaning: comprehensive protection from The Memory Corruption Vulnerabilities, which include overflows, the lifecycle vulnerabilities like UAF and type confusion, and uninitialized variables.
To the extent Fil-C and Rust both address these vulnerabilities, and don't include design features that in any practical way admit them, they're memory safe. What always feels like is missing from these kinds of analyses is that there are lots of memory-safe programming environments. Almost every Java, Python, Ruby, Javascript, and Go program is memory safe, in the real meaning of the term.
The competition to lock in and promote adoption of the "most" memory-safe language is a category error... unless you do what every language-war argument does, and redefine the term.
Even within Rust some folks do this to themselves, misusing unsafe with code that is 100% safe Rust, only to taint code that should only be called in specific cases, completely unrelated to memory safety.
I never seen this happen with GC/RC languages that also support unsafe code blocks.
As long as rowhammer is still out there, aint none of your memory safe. Fixing rowhammer is the memory safety absolutism I want to hear more about.
ECC memory fixes rowhammer. And random bitflips. Everyone should have ECC memory, but Intel disagrees because they are greedy.
My system sometimes detects a few bitflips per day.
There have been demonstrated rowhammer-based ECC bypass attacks. ECC mitigates but does not fix rowhammer.
My system sometimes detects a few bitflips per day.
Perhaps you should replace your RAM, or it's a sign that you have a massive source of radiation lurking somewhere?
I assumed this is just normal in DDR5 and the reason I only had motherboards with ECC to choose from. I do have quite a lot of it.
It wouldn't be the first time engineers pushed something close to the unreliability limit and compensated with a mechanism to make it more reliable.
Defective memory is another issue to be aware of and somewhat common. Around 10% of Firefox crash reports are due to bit flips.
https://news.ycombinator.com/item?id=47252971
I don't particularly care about Rust vs Fil-C shit flinging, I don't even see them as competing since they have effectively near opposite tradeoffs.
You could even use Rust alongside Fil-C to ensure the unsafe blocks are safe. It would even be interesting to turn off some of Fil-C's expensive protections in the safe parts of the Rust code, making an average-of-both-worlds sort of solution.
What I do care about are C and C++, these two absolute garbage languages (though I do have a lot of love for C, you can both love and hate something).
Tools like Fil-C, hardened mallocs like SlimGuard, and other "make C safe" solutions all sacrifice absurd amounts of performance because these two moronic languages refuse to have a proper slice/span type.
Use-after-free and double-free are serious problems but they're a spec of dust compared to missing bounds-checks. The low-hanging fruit is right there for the picking but everyone is worried about how to reach the fruits at the top.
C++ only now in C++26 is finally adding bounds checks to the [] operator on std::span and (hilariously) is also finally adding the now mostly redundant .at() method which should have been there from day one.
Clang added -fbounds-safety which is a feature that should have existed for a long time and serves as a decent stop-gap to a proper slice type. But of course, since it's not standardized, most people won't use it.
What these two languages have taught us is that if you want to produce utter garbage you should make it an ISO standard.
Me, personally, I find this whole discussion on memory safety amusing. Missing bounds checks are by far the biggest source of vulnerabilities and they're a problem in exactly 2 languages and those 2 languages have outright refused to do anything about it for decades.
But hey, even in memory safe languages you have frameworks like log4j that had remote code execution from a format string as a feature. Is the problem memory safety specifically, or this utter cavalier attitude towards security?
I'm not even suggesting something dumb like "you just gotta get good at C and then you won't have any vulnerabilities". Humans are fallible, which is why we delegate what we can to machines. I'm just pointing out the utter lack of care. People just don't care.
At least do the bare minimum of effort like, I don't know, not make remote code execution a feature tied to format strings. Or provide a slice type in your language and string manipulation functions in your standard library that make use of it, preferably 20 years ago.
C++ has had bounds checking in standard library for ages, and on compiler specific frameworks like MFC and OWL, however plenty of devs apparently never read the compiler manuals on how to improve safety and related compiler settings.
It is also relatively strange that many devs use the lacks of standardisation as an excuse to not adopt safety features in C and C++, while at the same time rush out to adopt cool toys that are specific to a single compiler.
> You could even use Rust alongside Fil-C to ensure the unsafe blocks are safe.
Isn't this what MIRI already does?
Partially yes. Fil-C goes further in creating an entire ecosystem of Fil-C compiled "legacy" libraries, meaning all the unsafe FFI your Rust code does into C is also safe, and even many linux syscalls.
Miri is meant as more of a sanitizer type tool that you use during development to catch bugs. Fil-C is a platform you compile your entire Linux distro with for use in production.
As long as the corporates continue to erode personal freedom with their hostilities, I consider these "memory safety absolutists" authoritarians, because humans will always make mistakes one way or another, and that should be the natural state of things, as attempts to banish all human error under some guise of "safety" will eventually lead to a horrible dystopia.
"Freedom is not worth having if it does not include the freedom to make mistakes."
"They who can give up essential liberty to obtain a little temporary safety, deserve neither liberty nor safety."
You should learn C and then use safe languages for serious stuff anyway.
Face the truth and see the reality of what you're making: better nooses to put around everyone's necks.
> Our historical data for C and C++ shows a density of closer to 1,000 memory safety vulnerabilities per MLOC.
Unfortunately, I've never seen a version of this data targeting modern C++ (>=11, with smart pointers, already 15 years old).
Because it doesn't exist, I have never seen modern C++ in real life as in conference slideware.
Most projects keep being full of C idioms, regardless of how much advocacy we keep pushing for.
Something that is important to me is that it's caught at compile time. A memory issue is a logic bug. Fil-c simply moves that from undefined behavior/security issue/incorrectness to a crash. That's better than what it was. But I generally don't want my programs to crash. Having memory safety at compile time is much more worthwhile imo.
As a Rust fanboy, I have to concede that Rust also doesn't have pure compile-time memory safety. Some checks are runtime there as well, in particular most of bounds safety.
I agree. Rust does not go far enough imo. Still catching most memory problems at compile time is still much superior.
The important thing from a performance standpoint is to be able to hoist bounds checks out of inner loops. This avoids a check on each iteration. Languages which have constructs such as
can usually hoist checks out of inner loops almost for free.I used to argue with the C++ committee about to when it's OK to catch an error early. If you write
that's a buffer overflow. The question is, is the compiler allowed to generate checking code which will abort the program before entering the loop? Or does it have to execute all the iterations up to the subscript error?I argued that it's legit to catch an error at the point it becomes inevitable. This leads to bikeshedding objections: "But what if a signal interrupts the loop before it runs off the end". That's why you need a language where undefined behavior has been nailed down to do this optimization properly.
True... but if you go by what you're most likely to encounter, I'd argue the biggest unaddressed flaw in Rust is the lack of a current, maintained analogue to tools like Rustig! and findpanics, which would analyze your compiled, optimized binary and report any code paths which lead to panics.
I don't like having to contort my code to fit each unit of work into the API of `std::panic::catch_unwind` so I can responsibily distrust the transitive dependencies beyond the reach of my Clippy lints.
If you really believed that, you’d be programming in something like ATS, Idris, or Spark Ada. The reason that (I’m guessing) you don’t is that it takes a lot more effort to write general purpose code in those languages.
Rust is pretty much state of the art in this area for general-purpose, non-GC programming languages, and people still complain about the effort involved in writing memory-safe code with it.
If you really want complete memory safety, use a garbage collected language.
Additionally, there are plenty of garbage collected languages with support for doing low level stuff, or RAII, when it is really required.
I program mostly in Rust, Haskell and Python nowadays. I indeed would love to go gc-less dependently typed. But I work in a team where the code I build is expected to be maintained by other devs. And going that route is unfortunately not tenable.
It’s the exact opposite for me. Catching it at compile time is great, but the most important thing by far is that it’s caught somewhere instead of creating a security issue.
I don't know if there can be any memory safe languages. Programming is always unsafe because you can always make mistakes. The best we can do is to use tools that help us avoid the common mistakes.
As an example of that, the rust compiler helps the programmer to avoid many mistakes but it's still possible to use memory incorrectly and the tool is only safe as long as you use it as intended. You could say that C is safe too as long as you use it as intended and make no mistakes. The only problem is that it's hard to use it as intended and make no mistakes.
So let's think about what memory safety could mean. My understanding is that a memory bug is when you use memory in an unintended way. An example of that is when you write to memory after you have deallocated it, which means after you have decided not to use it any more.
No compiler or tool can make sure you don't write to or read deallocated memory, because whether it's allocated depends on what your intention is, and no compiler can read your thoughts. They can only give you a tool (like functions for allocating and deallocating memory or compiler checks) and hope that you will use that tool in a way that reflects your intention. One problem is that those tools are not always sufficient to keep track of your intentions and you may not always use them as intended.
Memory allocation is relative. In a sense, no program that runs under an operating system can use deallocated memory, because when they read or write to memory that has not been mapped to the process, they crash. In a sense, even "safe" rust can use deallocated memory; let's say you have an array of 10 integers and you decide that the fifth integer should not currently be used, you have then deallocated it on a level where the available tools can not help you, but you can of course still access that memory.
So again, everything is safe if you use it as intended and nothing is safe if you use it in other ways. Safety depends on how well the code matches your intentions. Remember that programming always happens in layers and no tool can cover all of them. It's not even clear what "all layers" would mean or how many there are in a program.
> but it's still possible to use memory incorrectly
Only by misusing unsafe. Using it in regular programs actually isn't that necessary. In languages like C you have unsafe code almost in every line.
> My understanding is that a memory bug is when you use memory in an unintended way
Memory safety rules are more strict and formalized than you think. Memory safety issues are typically reading uninitialized memory (or strictly speaking changing observable behavior based on contents of uninitialized memory), reading/writing memory after it have been freed (free/delete call or out-of-scope going for local variables), concurrent unsynchronized memory access.
> No compiler or tool can make sure you don't write to or read deallocated memory
I am author of a programming language, where you can't write to or read deallocated memory, unless you misusing unsafe.
> no compiler can read your thoughts
But many languages more complex than C have powerful features allowing telling the compiler your about intents. At least partially.
> Memory allocation is relative. In a sense, no program that runs under an operating system can use deallocated memory
You are mixing two different concepts. One is memory model of the abstract machine defined by the specification of a language and other is the OS processes model. They have much in common, but there are a lot of differences.
> So again, everything is safe if you use it as intended
Such mindset is considered harmful, since it provides an excuse to use languages where making mistakes is easy (like C).
I think the point you missed or don't want to accept is that I think of all layers of memory management as the same thing just on different layers. Where you say that I mix different concepts, I say that it's just different layers of the same problem.
So which layer should we care about? All of them? Yes, if we want the most safety.
I will give you an example of how I can use memory incorrectly in "safe" rust. Let's say I have an integer that contains my age. Now I forget what the number was used for and I use it as shoe size. I have now used memory (the content of the variable) as something it was not intended for. Maybe you think this example is silly, but what is really the difference between using a float pointer as an int pointer and using age as shoe size? Another thing we could do is to use an index for one array as an index for another array. That's an example of using a pointer as the wrong type because an index is just a relative pointer.
So if we want to care about all layers of memory safety then we have to care about not using ages as shoe sizes and indexes with the wrong arrays. "Memory safe" languages usually don't help us with that and therefore they don't give us total memory safety. I think "memory safety" is not a very useful term because all data in a program lies in memory and basically all bugs have something to do with using that memory in wrong ways.
> we have to care about not using ages as shoe sizes and indexes with the wrong arrays. "Memory safe" languages usually don't help
In languages even slightly better than C one can create a wrapper type for int/float with additional semantics like age, shoe size or something else. Since they are different types, using one in place of other isn't possible. This doesn't solve all problems, but at least can prevent silly mistakes.
Interesting, I have had ideas about having that feature in a language: being able to make subtypes, like "this is an integer but on the next level it is a shoe size". Can you give an example of such a language or how they usually do that?
Ada from day 1: Subtype with checks: https://www.adaic.org/resources/add_content/standards/05rm/h...
`type Shoe_Size is new Integer` defines a type incompatible with `type Age is new Integer`
You can also simply wrap an int in a struct in C.
The problem with this post is the extent to which it shows that its author has an unhealthy obsession with me personally. It’s weird but also oddly flattering.
I don’t dislike Rust, and when I point out that Rust is not fully memory safe, it’s because I find the details here super interesting. It’s interesting that Rust deliberately chooses to have an unsafe subset. It’s interesting how that leaks out to the rest of the language. It’s interesting because us language designers ought to be thinking about how this could be avoided. It’s a hard problem! And that makes it fun!
If you do read what I say on twitter, you’ll find praise for Rust, many concessions about Rust being faster than Fil-C, as well as a wide range of other opinions. When I point out Rust’s unsafety, I’m just citing facts. It’s interesting how doing that really seems to upset some folks.
Pointing out a limitation in a technology does not imply dislike, and it’s best not to take it personally.
I don't have an obsession with you. I haven't read the entirety of your social media presence, but almost every time I see a discussion about Rust on Twitter, especially involving Fil-C, I see your criticism of Rust, without much nuance. To be honest, seeing your replies emphasizing Rust's memory unsafety under pretty much every tweet I've seen about the subject felt like an obsession with Rust, to me.
The problem I have with the framing I've seen countless times from you on social media is that I have never seen any nuance on what Rust's memory safety model achieves. You say you state facts, but stating facts without context can still be dishonest. Claims like "both C and Rust are memory unsafe languages" or "both C and Rust allow introducing memory safety vulnerabilities" are factually correct, but pretty much useless in practice.
I'm sorry for misinterpreting the "dislike" part. In retrospect I shouldn't have even brought like or dislike into it cause it was not the main point anyway. The main point was framing that I've seen on Twitter, not only from you, that in my opinion is quite one-sided.
I removed the dislike part from the post now.
> I don’t dislike Rust
As someone who really enjoys seeing all of the different programming languages and their approaches to different problems, it’s becoming obnoxious to hear the constant battles among people who think programming languages need to become part of your identity. The endless battles of superiority and tit-for-tat responses feel petty and distracting.
It’s refreshing to get back to people who just want to try different things and experiment without making everything into a battle where each side scores points against the other.
We did that to ourselves, picking languages like football memberships assigned at birth, with religious fights on the playground.
Not only languages, editors, OS, hardware platform,...
Because when it comes to apply for a job, the HR drones don't understand that one can manage to program in more than one language, use more than a specific OS, and so forth.
It was to be those specific bullet points, and by the way at least during the last five years exactly before applying to the position.
There are also the trivia questions during the interview about obscure language features.
So this naturally spills into one's identity, going back to the primitive mind of the days humans lived in small groups and it was us versus them across all tribes.
Which for those of us that are generalists, is a big pain in some body part.
Memory safety can be defined in more than one way, so it’s super worthwhile to figure out how to define it and what meets the definition and what doesn’t.
We should do more of that as a community. It’s important stuff. Ima do my part so you’ll likely see me poke at how Fil-C does a thing that Rust doesn’t do. If you read my arguments unemotionally, I think you’ll get a deeper appreciation for Fil-C, Rust, and memory safe language design generally.
What we shouldn’t do is reduce the discussion to claiming in a blog post that so-and-so “doesn’t like” such-and-such.
I think Fil-C is awesome and I very much like the style in which you present your work as well. (Hope I'm not being too emotional here!)
FWIW, I can see where TFA is coming from and I had the urge to write something "in defense of Rust" after listening to / reading some of your recent comments. I think your sister comment about the limits to Rust memory safety being real as much as the limits to Fil-C's performance and practicality is spot on, but the feeling (those emotions again) one gets from much of your commentary is that you downplay the latter quite a bit.
I think the real argument for Fil-C is that you aren't going to "rewrite in Rust" but you can often realistically recompile in Fil-C and pay the performance penalty; the fact that that Rust rewrite could also end up less safe is I think should be argued mainly from the AI angle where the only practical way is an LLM based rewrite but that would have a load of unsafe - way more than a "proper" rewrite - to be practical and the recent $100K Bun rewrite is a testament to that.
(I would love nothing more than for Fil-C to become as widely used as it practically can, since no other approach does nearly as much to secure C / C++ code and a lot of said code is out there; and security aside, it could prevent a lot of data corruption that most major C++ programs gift to their users.)
> I don’t dislike Rust
I do. Not for any technical reason, but because the Rust evangelists seem ready to burn me at the stake if i don't agree that memory safety is the one true god and Rust is it's prophet.
They hurt the language adoption more than they help it.
How is this better than the GC in D?
It is better in a sense that D is not C or C++. Technical innovation is memory safety with high level of compatibility with existing C and C++ in practice. It is compatible enough that you can run memory safe LibreOffice with Fil-C. D doesn't do that.
It's annoying how many comment sections online are now just Rust vs Fil-C / Zig flamewars, and the creators of those languages are deliberately fanning the flames.
I also feel there's a second dimension to the politics, where Rust is the "woke" language and C / Zig / Odin are now the "anti-woke" languages. At least, going by their most vocal online communities.
I'm sure offline there's still engineering decisions being made (I hope).
Could you point me at the members of the Rust project doing so? I'd want to have a word with them (I'm a member of t-compiler).
IIRC, ralfj was insinuating that Java was memory unsafe and arguing about it with experts on HN not too long ago. But I think he was coming off hot from the T-spec debacle so I understand it as him wanting to blow off steam.
The most toxic Rust leaders exited from the project a few years back, though some are flaming more freely than ever, e.g., Go barely deserves to exist, “SQlite is a terrible example” etc
Sorry for not being clear, I meant the creators of Fil-C and Zig (since those are the ones mentioned in the article)
They aren't. It's some random rust users vs the leader of the Zig project, leader of the Fil-c project, the head of community at Zig, etc. But they like to pretend there is an equivalence on the Rust side.
Andrew Kelley is a strong advocate for the LGBTQ and has railed against ICE in Minnesota.
I’ve only heard the woke/anti woke framing from Lunduke on the right, and a few Rust fanatics on the left. Both parties and their sympathizers want to fit the Holy Language war into the Culture war™
Repeat after me: Rust is not just memory safe C++!
Memory safety is a really big reason to use Rust, but it is very very far from the only reason. Even if Fil-C was magically zero-overhead (that is basically what CHERI is), I would still rather use Rust.
Rust has so many advantages over something like Fil-C it's hard to list them. I think Fil-C is a great project but it's only really relevant if you have no choice but to use C. If you can use Rust you also get:
* Compile time memory safety (Fil-C / CHERI are run-time).
* A modern functional-style type system (helps prevent logic bugs).
* Tree ownership (helps prevent logic bugs)
* Sane toolchain (helps prevent hair loss).
* Easy dependencies.
* Vastly more things checked at compile time.
Also I find it amusing how this post claimed it wasn't about Rust and then spent the whole time talking about it.
Rust is great. I don't think that really needs to be debated any more.
> And I also think it is totally fine to use Rust even if you could use a GC language like Go or Fil-C.
The problem with choosing Rust over a GC language like the above (or a good one, like OCaml) isn't that it's not "fine" to use Rust, but that manual memory management is an inefficient use of developer time. That's an issue for the developer, and one he inflicts on himself, not an issue for the end-user. Both Rust and (safe) GC languages provide memory safety, after all.
Rustafarians do seem to be worried by Fil-C and for good reason.
One of the top two issues with Rust is its cumbersome and overbearing syntax and anything which sidesteps that is automatically attractive to anyone which is worried about memory safety and doesn’t like to encode every little detail about in the type system. That’s the majority of programmers… think about it, that’s one of the big reasons people switched to GC languages which are the most popular languages.
The classic Rust approach to addressing criticism of the syntax was a mix of downplaying, gaslighting and “you’re holding it wrong”. But if easy to use, reasonably ergonomic alternatives become available, I expect that most would prefer them to Rust.
Funny to see how Rust devs start to make up new labels when it turns out some language performs better then their language.
Someone should fire up an AI and create Fil-Rust by connecting the Fil-C llvm backend to rustc, ending this flamewar. I guess it is well within the capabilities of a Fable class model, even if the driver doesn't have experience in compiler development.
> But Rust is unsafe, isn't it? It has unsafe after all! If you want to be that strict, or in other words, if you are a memory safety absolutist, that may well be true for you. I, and I hope most people, am more pragmatic than that.
So... Yes, Rust is less safe. Just that now the Rust apologist wants to back away from that and say that actually memory safety isn't actually the end all be all. C has a lot of security problems. Rust has less, at the cost of breaking the ecosystem. Fil-C has even less, and breaks the ecosystem. Why is the place Rust stops now suddenly good enough?
Like everything, it's about trade-offs. C is really unsafe. Rust is almost entirely safe, and breaks the ecosystem just a little bit. Fil-C is entirely safe, and breaks the ecosystem entirely.
You can just...not use it though? It's not comparable to trying to avoid it in C/C++ because there's literally just one keyword to never use and then you're good, compared to it being possible to silently introduce in any number of ways. I put a lint in my Cargo.toml to forbid unsafe code in my projects, and now I'm guaranteed not to write unsafe code.
To apply your logically consistently, you'd also have to throw out Go[1], Java[2], C#[3], and plenty of other languages. I guess you can define a logically consistent taxonomy of languages where Fil-C is the only "safe" language any everything else is unsafe, but then the burden is on you to prove that it's a useful way of thinking about them. Meanwhile, the way that people who consider Go and Java and Rust to be safe and C/C++ to be unsafe will continue to see actual real-world differences in outcomes when using a safe language compared to unsafe ones.
As an aside, it continues to be absolutely wild to me to see how most arguments in favor of C/C++ over Rust nowadays are based on framing things theoretically rather than practical ones. It was not all that long ago that the situation was reversed, with people claiming that Rust's benefits were all theoretical when most C/C++ code would have all of the UB found and removed over time. Now that Rust actually has been getting used for real-world stuff for a few years, and the number of vulnerabilities found in C/C++ code does not seem to be going down any time soon, any actual empirical evidence gets handwaved away. Maybe Fil-C will be able to provide as large benefits to running C safely in production like its proponents claim in the future, and if so, that will be a solid argument against the utility of Rust in a lot of situations, but if we're collectively going to shift the standard to discount pragmatism over theory in this debate, we might as well not try to hide it.
[1]: https://pkg.go.dev/unsafe
[2]: https://docs.oracle.com/cd/E92951_01/coherence/java-referenc...
[3]: https://learn.microsoft.com/en-us/dotnet/api/system.runtime....
> To apply your logically consistently, you'd also have to throw out Go[1], Java[2], C#[3], and plenty of other languages
Yes? Is Fil-C not obviously safer than those?
> I guess you can define a logically consistent taxonomy of languages where Fil-C is the only "safe" language any everything else is unsafe, but then the burden is on you to prove that it's a useful way of thinking about them. Meanwhile, the way that people who consider Go and Java and Rust to be safe and C/C++ to be unsafe will continue to see actual real-world differences in outcomes when using a safe language compared to unsafe ones.
It seems like "can switch on unsafe whenever" vs "has no escape hatches" is an obvious line in the sand and a very useful distinction with real world implications.
> Maybe Fil-C will be able to provide as large benefits to running C safely in production like its proponents claim in the future
Why in the future? It exists and can be used right now. In fact, much of its appeal is being able to use existing code without needing to port to a new language; pragmatism seems like an argument in its favor.
> It seems like "can switch on unsafe whenever" vs "has no escape hatches" is an obvious line in the sand and a very useful distinction with real world implications.
Sure, although to me, the obvious distinction is that one of them gives you more flexibility (since neither Go nor Fil-C is at risk for someone accidentally writing unsafe code).
> Why in the future? It exists and can be used right now. In fact, much of its appeal is being able to use existing code without needing to port to a new language; pragmatism seems like an argument in its favor.
Because "can be used" is not a statement of present use, but potential future use. You seem to be missing my entire point about theory versus practice; plenty of things might be useful in theory but don't ever get widely used in practice (e.g. because they're not user friendly enough, or they require constraints that aren't acceptable to most users). I'm not making a claim about whether Fil-C has these issues, but making a claim that as yet, there does not seem to be widespread usage of it. If it's really so seamless to drop-in as a replacement for C that's strictly safer without downsides that make it unappealing to most people, I'd expect that to change. If that doesn't happen, I'd consider that evidence that it's not actually providing meaningful safety in the real world (in the https://xkcd.com/1312/ sense)
Bearing in mind that "there should be no room for a language above assembly and below Rust" (i.e. something like what C or C++ is to Python, Java, C#, etc.) was an intentional design decision for Rust which resulted in `unsafe`.
Actually there is, given that the reference compiler depends on C++.
It seems very common for people to misunderstand that having strict accounting for unsafe is part of why Rust does what it does so well rather than just disallowing unsafe outright. I think it's easily confused because being able to choose "how much unsafe do I want to allow, and where do I want to allow it" allows the choice "I don't want any in my own code" to be easily enforced, and that's by far the most common amount needed, but it's certainly not the only useful one. Being able to handle the edge cases without having to give up the checks entirely is a huge improvement over most alternatives.
I don't immediately follow how Rust breaks the ecosystem. Rust has very good C FFI support and fully supports the default C ABI. It just requires unsafe bindings.
In practice, my Python packages broke when they dropped the C crypto support. I'm sure it was theoretically possible to avoid that, but they didn't. And that's ecosystem breakage. (And if the fix is that I the user must go manually install or worse compile extra things, that's definitely ecosystem breakage)
Unsafe doesn't disable the borrow checker
I don't follow how that impacts the argument. Rust with unsafe is certainly safer than C, but I don't see that that changes anything at hand.
There are only three programs where memory safety matters: HTTP server, browsers, and operating systems. In practice, really just browsers and operating systems. Memory safety schemes that don't work for those systems are primarily cosplaying if their goal is safety.
That's really just not true. Easy counterexample: any codec should be written in a memory safe language.
Really anything that deals with untrusted input should be memory safe. Your TLS library. A load balancer. Your password manager.
In fact codecs in particular should be written in something much stricter but more special purpose than Rust, WUFFS (Wrangling Untrusted File Formats Safely)
https://github.com/google/wuffs
WUFFS gives up generality - you can't write "Hello World" in WUFFS because it lacks both strings (for the "Hello, world" text) and I/O (for the printing it out). But you can write a codec, going from a block of bytes representing the encoded file to a block of bytes representing pixels, or PCM audio, or indeed uncompressed data [or vice versa] is easy.
But unlike Rust, all of the safety in WUFFS was checked during compilation. For example Rust emits bounds checks, arr[n] might panic at runtime if n is outside the bounds of arr, but WUFFS doesn't do that, it'll have proved mathematically that n is always in-bounds, if it can't prove that it rejects your code, make sure n is in bounds and try again.
Sometimes the end result is similar, you write code to check n at runtime, if it's a miss you report an error, WUFFS can see you met the criterion - basically the same as Rust. But often in a codec design you can just prove it's in bounds, if you implemented it correctly.
Yes, DSLs or formal verification tools are a much better solution for many scenarios.
And how about safety of life systems? They use computers in chemical plants and on planes, you know lol. Integer overflows have a body count!
In these applications we want what's called "Functional safety" where what we care about is that the humans are kept safe. A Memory Safe language can be useful to help achieve this, which is why https://ferrocene.dev/ exists but it's also important to have business processes to assure that what the software is supposed to do will keep the humans safe, memory safety doesn't distinguish between "Ensure the human operator is in the containment zone when a cloud of toxic vapour is released" and "Ensure the human operator is NOT in the containment zone when it is released". But for that operator this difference is crucial.
I kinda wish we didn't push with the term "memory safe", and instead had a push with "correct". If your software isn't memory safe, it does not work correctly. We should aim to have software that works correctly.
The word "correct" already had a meaning. Memory safety is a subset of correctness in the same way that Rust can't statically prevent race conditions but it can prevent a subset of them called data races.