Someone in the audience also pointed out that a tool like this could be used to compile rust programs, not just C programs, in which case it's odd to hear Fil-C repeatedly framed as a language in opposition to rust rather than as a tool which might complement it.
Adding Fil-C-like runtime checks to Rust is definitely an interesting direction.
That's the thing, though. Your Fil-C code isn't calling mmap, it's calling the Fil-C wrapper for mmap. In neither Fil-C nor Rust('s safe subset) can you call the actual system mmap.
mmap is a bit of an outlier because it is not possible to implement a fully featured safe wrapper (MAP_SHARED) in Rust. So I would be curious to see what safety guarantees Fil-C claims to provide for mmap.
You might call that library "nix"[1]. Many, though not all, of the bindings are safe wrappers around the underlying unsafe syscall.
(The specific call of mmap from upthread, though, that one is not. I'm not sure how you would make such a call safe.)
Just write `#![forbid(unsafe_code)]` at the top of your src/lib.rs, and track crates not using it with `cargo geiger`. You don't need a whole new language, it already provides the tools to wield that hatch shut.
> Rust is less safe because it has a feature for turning off safety
Can I write Fil-C's mmap wrapper in Fil-C?
If no, fair enough, but it's worth noting I can write a safe Rust mmap wrapper in Rust, and I absolutely and frequently need to do that kind of "syscall wrapping" in arenas Fil-C explicitly hasn't handled, by virtue of being explicitly a Linux project.
If yes, that sounds like an escape hatch to Fil-C's memory safety, undermining the claim that Fil-C is fundamentally safer, and you're now at best arguing it's safer as tends to be used.
• `std::mem::uninitialized()`. It's now deprecated in favor of `MaybeUninit`.
• `std::env::set_var(...)`. It's now marked `unsafe`. While I dodged the bullet, these fellows didn't: https://www.geldata.com/blog/c-stdlib-isn-t-threadsafe-and-e...
• `#[no_mangle]`. It's now `#[unsafe(no_mangle)]`.
I don't believe I've constructed cvs-rs's hole by accident. cve-rs's code links to https://github.com/rust-lang/rust/issues/25860 , which is admittedly still open after a decade. Progress appears to still be ongoing though! Related:
• https://github.com/orgs/rust-lang/projects/44/views/1
• https://rust-lang.github.io/rust-project-goals/2025h2/next-s...
> Meanwhile, here's a memory safety bug that Fil-C fails to catch, not because of a compiler bug, but by design: [...]
Thanks for sharing a concrete example of Fil-C's limitations! Disappointing - but not unexpected - to see it underperforming existing sanitizers. I was hoping for better.
Indeed, I'm trying to be careful not to claim that merely fixing #25860 at long last would make Rust's implementation suddenly beyond reproach. For that, we would need formal verification (which, so far, has at least been done by Ralf Jung's group for proving the correctness of large swathes of the standard library, but I wouldn't expect that to extend to the compiler anytime soon).
So I don’t see how Rust is at a huge disadvantage here when Fil-C is typically not going to be enforcing safety in production. I suppose the testing/fuzzing story becomes all the more important because you need to exercise runtime behavior for Fil-C to detect unsafe faults.
Don’t get me wrong: I think Fil-C is a smart idea and an excellent way to introduce memory safety to existing C codebases. But I don’t get this whole (usually implied) idea that Fil-C somehow makes Rust look bad or renders it obsolete.
C is more verbose and more error prone than Go and C#. It has worse tooling. It’s missing decades of language features. C isn’t properly cross platform. There’s no package manager. The “standard library” isn’t fully standard. It’s full of sharp edges and bad decisions.
I can imagine using Fil-C to run legacy code. But for new projects, it just seems worse in every way compared to Go, C#, typescript and friends. It’s worse, slow, and inconvenient.
But what is being overlooked, is the massive numerical dominance of C programmers and projects, along with legacy and embedded code. There is going to be a preference for writing and using C, that could arguably fuel Fil-C for a very long time.
I roll to doubt. I haven’t seen C topping programming language popularity charts for a long, long time.
I used to interview software engineering candidates professionally. Candidates could pick any language they wanted for the interview. Python was chosen by about 70% of our interviewees. C was under 5%. (N=400 or so)
C is still highly ranked on many charts. C (as of July 2026) is ranked #2 on the TIOBE index. PYPL has C and C++ weirdly merged, the annoying C/C++, and that comes in as #3. The IEEE and Redmonk have C firmly in the top 10, using different methodologies. We also have to consider how many years back that C's run in the top 10 goes.
We have to also use context, as popularity can be a measure beyond simply job demand, relative to usage by students, hobbyists, open-source, and legacy. Furthermore, demand can be relative to location and country. Because it is less in your specific area or company, doesn't mean that's so in other areas or countries.
JavaScript at #6, when it should be at #1? Trailed by Visual Basic at #7? SQL (not a programming language) at #8 and R (have you ever seen an R job?) at #9. Then you have Rust just barely squeezing past Delphi? Followed by scratch???
Come on, this is not a serious source. Neither are the the rest really. Any popularity/usage list that doesn't have JS at first place is not serious.
Yeah I know the feeling. But you don’t need Fil-C to learn C.
That said, with a GC, you could make some great tools & visualisations for learning C and understanding the memory layout of a running C program. That would be cool to see
If python/go/C# is easier to write, why choose C? Fil-C removes C’s performance and interoperability advantages.
TBH C# is probably a good default for a lot of projects, from a safety and performance standpoint (rust's big performance win over GC/JIT languages is more in the memory consumption than throughput, counterintuitively). Fil-C probably still has a pretty good edge on python's performance, though.
Probably - but where are the benchmarks?
> Python […] generally doesn't scale well (in terms of size of team and codebase).
I’d argue neither does C. Everyone works around C’s lack of generics in their own way, so every large project becomes its own little world.
C’s biggest scalability problem is its lack of clear ownership semantics in APIs. If I call a function in your module which takes a Foo*, am I passing ownership? (So you’re responsible for freeing the object)? Do I need to retain the object while you use it? How long? Can I free the Foo when the function call returns? Or do you hold onto that reference until much later? Am I free to keep interacting with the Foo while you hold a reference? Can I do that from a separate thread, or is that unsafe? Every time I call a function in a medium to large C project (or in a library), I need to answer all these questions. Documentation is often unclear and the language doesn’t help at all. Mistakes lead to crashes, corruption and CVEs.
Real GC languages (Java, C#, JS, etc) solve this problem by just letting me write my code however I want. The object is freed when no more references are held. So I don’t even have to think about it. The cost is worse performance at runtime. Rust solves this with lifetimes and static analysis. You pay with complexity and slow compilation. But programs are fast and correct.
C - and by extension Fil-C - makes me solve this problem by hand. But you also pay a runtime performance cost to use a GC. This seems like the worst of all worlds.
Obviously people still write large programs in C. But I’d argue they manage that through grit and skill, in spite of C’s weak ownership semantics. Not because of it.
Or you could write a library that works for Fil-C and C without writing the library twice.
Also, it’s probably fast enough for apps to be deployed in something like a staging environment for integration tests to run against them. Production deploy/release can then happen using a tagged version that gets compiled using vanilla GCC/Clang.
It is possible, check MaulingMonkeys' comment upthread.
Even so Fil-C fails at being 100% compatible for userspace due to the silly things people do with pointers. Hence the large amount of effort he’s had to do to fix up that 0.1% of userspace that breaks.
You can’t use mmap in a way that corrupts memory in Fil-C
Try it. :-)
Of course I'm kidding, but it is absolutely the case that if you truly care about safety you need to consider more than the program source code and binary, but also the environment, including kernel and hardware. The user doesn't care if their web browser got hacked via stack buffer underflow or /dev/exynos-mem or Rowhammer; the result is the same.
Point is, Fil-C goes further than any other memory safety tech in terms of what it guards
So Fil-C is safer than those
> Fil-C is safer than those
Says you. It seems presumptuous to me to be so dismissive of their work. The Go and C# teams do good work.
But I wasn’t even asking about safety. I’m curious about ergonomics and performance. Fil-C isn’t the only safe wrapper around raw syscalls. How is cross OS compatibility with Fil-C? How nice are the APIs to use? UNIX syscalls are pretty badly designed imo. The error paths alone are a mess.
Go’s situation is nuanced since a lot of Go code does rely on unsafe C or C++ deps, but I have no idea how generally true that is. Also Go’s protections fall apart under certain races, which isn’t true in Fil-C.
Fil-C's safety guarantees therefore fail to apply in this situation.
In a race, you at worst access an object you could have loaded from whatever field you were racing on.
In Go, you can bypass all protections and corrupt all of memory if you race on structs (I think).
Suppose a pointer ptr is initialized to foo and transitions once to bar; suppose also that x is attacker-controlled. Then,
T1: T* p = ptr; if (p == bar) p[x] = 7
T2: ptr = bar
A program can observe p == bar (testing the address bits) but still permit a write to foo through p (allowed by the stale capability bits after offset by attacker-controlled x), allowing T1 to perform an unexpected mutation of foo.No, you do not always trap in this scenario, as you've claimed repeatedly on X. You perform the capability check after combining p and x. If an attacker sets x == foo - bar, then p[x] refers to memory inside foo even if p == bar.
Because (for understandable reasons) you don't insert a memory barrier between a write of a pointer's address bits and its capabilities, use two-word atomic accesses, STM, or in any other way synchronize writes to pointer addresses and capabilities, programs with data races can observe arbitrary combinations of linear addresses and capabilities that go along with them.
Plenty of exploit chains have had humbler beginnings.
This access-foo-through-pointer-to-bar scenario can't happen in Java. It can't happen in CHERI. It can happen in Fil-C. Yes, T1 at one time had a capability on foo, but the programmer intent is clearly to mutate only bar, and Fil-C allows an execution that mutates foo instead.
Elsewhere, you've claimed such executions cannot be exploited. I am skeptical of this claim given previous exploits that began by the camel poking its nose through similarly innocuous-seeming holes in the tent.
Fil-C cannot fully protect C programs from exploitable memory corruption caused by violations of the C virtual machine. 99.9% of practical ones? Sure! Fil-C is good stuff. But there are holes (not only here, but for arenas, intra-object corruption, etc.), and these are holes that safe Rust prevents. Fil-C and Rust rules prohibit different (but mostly overlapping) classes of exploit.
Could you define Fil-C's behavior as "memory safety"? Sure. You can define words to mean anything. You cannot, however, define Fil-C as something that just deletes the security implications of bugs in existing C programs. Does it mostly achieve this goal? Sure. Does it supply comprehensive coverage? No! It's a hardening tool, not a panacea, and it would behoove you to represent it as the useful tool it is, not magic pixie dust that makes C safe.
Please stop ruining a tool as good as Fil-C by claiming it does things it does not and cannot.
> In Go, you can bypass all protections and corrupt all of memory if you race on structs (I think).
This is my understanding as well. I'm glad we agree, opinions on Fil-C memory model counting as "safe" aside, that Go is awful.
> if (p == bar)
It is not in scope of memory safety to make sure that logic not related to memory accesses works as you expected.
In Fil-C, the integer pointer value (the intval) is not trusted. You could get it wrong with things more sexy than races (integer overflows or just plain bad math). Fil-C just guarantees that your accesses obey the capability model, which is true in your example - the only object the program can access is whatever object the capability you loaded points to.
You’re being disingenuously imprecise when you use this framing:
> access-foo-through-pointer-to-bar
In fact, you can only access foo if bar’s capability referred to foo, and that can only happen if the thing being raced on (the pointer in shared memory) had a prior store to it that had foo’s capability.
Hence, this isn’t an arbitrary memory access. This is a memory access that obeys the capability model. It’s a memory access that would have been possible even if the program had no race.
> Please stop ruining a tool as good as Fil-C by claiming it does things it does not and cannot.
You are mischaracterizing the issue to make it seem like it’s a memory safety issue, when it’s not. I think that is doing more damage than anything I have said
This logic is related to memory accesses. This is not an issue that can occur in systems that provide the memory-safety guarantees that the words "memory safe" usually name.
You can't escape failure to meet a public definition of a term by adopting a private definition. The English language has a term for this pattern.
> You’re being disingenuously imprecise
You are the one being imprecise. You claim that because T1 could access foo at some time in the past, it's okay for a subsequent execution to access foo when the program text limits the access to bar. The temporal history is irrelevant. You are taking language that applies to inter-actor access control and applying it to invalid execution detection.
If T1 and T2 were, say, mutually untrusting actors over a network, that T1 had a capability at one point would be a valid defense. We're talking about a totally different scenario, not an object-capability security system, but preventing attackers turning illegal C into various kinds of exploit, e.g. EOP, information disclosure, and so on.
It does not matter that T1 used to have the capability and could have used it. We're talking about an attacker using an exploit to make T1 do his bidding, not T1 itself being a hostile actor running in a sandbox.
You can't just define the problem away. Real-world C has these races. They've been exploitable. Your system doesn't close them.
> Hence, this isn’t an arbitrary memory access.
Not arbitrary, true. Irrelevant. It's a cross-object access that violates programmer expectations and is likely exploitable.
> This is a memory access that obeys the capability model
You're using the English words that denote a strong guarantee in the security community and using them to describe your weaker system.
> I think that is doing more damage than anything I have said
Damage to what? You can't fix this problem in Fil-C without introducing atomics to every globally-visible store path, so you're going around the internet trying to play definition games to define it out of existence. The only damage here is to people who think Fil-C provides stronger mitigations than it does.
Anyone could do the same with a Rust lint rule if he wanted. He wouldn't, because this policy is not the way to secure software.
“Custom” and “system” aren’t the terms I use; I say user libc and you libc (because they are basically the same libc - either both are musl or both are glibc).
User libc calls to the Fil-C runtime, which filters syscalls, and those filtered syscalls are made via yolo libc.
Hence, a Fil-C program is memory safe down to the syscalls and syscalls cannot be used to escape the protections (unless you do weird stuff with /proc)
Fil-C: Combining the ergonomics of C with the performance of Python!
It is inconvenient to start using it as it comes with basically nothing out of the box, but this is irrelevant after a short time using it.
And it’s fun to write new code in.
You can combine both approaches for sure, but that doesn't change that they are indeed separate approaches. One (Rust) intends to characterize and prevent undefined behavior at compile time, and another (Fil-C) intends to make undefined behavior impossible at runtime, as evidenced by decisions like (for example) making unsafe usage of setjmp/longjmp safe.
You could sort of achieve "safety-in-depth" by combining both approaches, but Rust prefers to prevent all undefined behavior statically, and Fil-C prefers to focus on dynamic approaches. They have real differences, so that's probably what comes off as "oppositional"
Thus anyone that identifies themselves with the programming language they work with, gets dragged into a us vs them discussion.
(I'm separately skeptical that it's the most important memory safety property; I suspect that a review of Chrome and Firefox 0days would show that UAFs and type confusion are, at least to attackers, equally if not more important.)
(You mentioned WUFFS below, which is why I qualified with general-purpose! One thing that WUFFS does that I think Rust could add pretty easily is provable indexing, e.g. allow me to use a `u8` to index a `[u8; 256]` without having to widen to a `usize` first and hope that LLVM optimizes it back out.)
My thought here is to proactively verify that LLVM elided the automatic bounds checks in places where you believe that your explicit checks should be sufficient.
That was a key part of my article on "No-Panic Rust": https://blog.reverberate.org/2025/02/03/no-panic-rust.html ("A Dance With The Optimizer")
Where? The GP's comment says "prefers," which I didn't read to mean a blanket statement. I don't think anybody with more than passing experience in Rust (or C) would make such a claim.
> Some dependently types languages can prove this statically, also model checking can do this, etc. So it can be done statically as well, but not in Rust.
You're couching the part where it can't be done with full generality or can be done with full generality, but with punishing semantics. The appropriate comparison here is with other normal general purpose compiled languages.
Dependently types languages and model checking do exist. They come with tradeoffs, but this is also true for Rust.
for i in some_arr {
println!("{i}");
}
I suspect most techniques you could think of to statically avoid bounds checks are possible in rust's type system. fn main() {
let v = vec![1, 2, 3];
v[5];
}However, a vector's length is inherently a runtime concept, and is a user-defined library type, so the compiler does not attempt to directly reason about this at compile time. However, for this example, post-optimizations, it doesn't do a runtime check at all: it calls the panic directly, because the optimizer does in fact reason that this always panics and removes the dynamic check.
It is true that Rust inserts dynamic checks for things that it can't prove statically, but so do dependently typed languages. "Please read an integer from stdin and then load that element of an array" is not possible to statically check, it must rely on runtime checks, definitionally.
me: It does dynamic bounds checking.
Rustaceans: But... for XYZ ... it doesn't...
Sigh.
Rust (outside of the unsafe superset) has no undefined behavior. That is guaranteed, statically, at compile time: all execution paths have well-defined behaviors. That does not mean that there are no runtime consequences whatsoever. For example, the behavior of an invalid index is a panic, at runtime. Inserting a check for an invalid index at compile time is static enforcement of the behavior, even if the check itself happens at runtime.
If you had said "sometimes, Rust can't eliminate dynamic checks because it doesn't have a strong enough reasoning about some compile time properties" nobody would be arguing with you about this. Your example is one where a language with stronger reasoning would be able to detect it, for sure. Lean can do this, for example.
If, of course, you redefined words so that adding run-time checks counts as "statically preventing all UB", then this applies to Fil-C just as well! You can not have it both ways.
Rust does prefer static checks. It uses static analysis to prevent many types of bugs - including use-after-free and data races. But you’re right; rust still falls back to runtime checks when static analysis is too hard. Like dynamic bounds checking and integer overflow (in debug mode). Cell, Refcell and friends also have a (small) runtime cost. Rust prefers static analysis but does not use it exclusively.
Let’s say rust is 80% static analysis, 20% dynamic checks. Fil-C seems to go the other way and have 80+% dynamic checks. It’s an interesting point.
Nobody disagrees with you that rust sometimes inserts dynamic bounds checks. But “prefer” in this context means “most of the time, when possible” not “all of the time”. I prefer vanilla ice cream over chocolate. But I still eat chocolate ice cream when vanilla isn’t available. Pointing out that one time I ate chocolate ice cream isn’t a gotcha moment.
Again: I'm not disputing the fact that the rust index operator does a dynamic bounds check.
On the other hand statically checking bounds for (dynamic and non) arrays is very very hard in the general case.
If I'm understanding correctly, it's this general syscall function that's safe in Fil-C.
fil-c runs on linux.
what if linux ran on fil-c?
It would sometimes be a good trade off, for some users. But I don’t think many regular users would choose to pay this cost.
(Left as an exercise for the reader)
> I'm running on an OS where the entire userland is compiled with Fil-C/C++
(emphasis mine)
Looking up https://fil-c.org/pizlix , that says,
> The kernel is compiled with Yolo-C. So that you can compile the kernel, a copy of GCC is installed in /yolo/bin/gcc.
Where do you see anything saying the Linux (the kernel) can be built with Fil-C?
However, I don't see why the full Linux kernel could not be compiled with Fil-C. It would be nice if Fil himself could explain what limitations there are, but the documentation does not list missing C/C++ features as far as I know, it only says it's "fanatically compatible" which I take to mean mostly everything should work?!
But to my point in general, here's a osdev.org wiki explaining how high level languages can and have been used for OS development (with the caveat that some Assembly code is required, which I believe is also true of kernels written in C): https://wiki.osdev.org/Languages
A new language designed around memory safety, such as Rust, can reject large and important classes of memory-safety bugs at compile-time. Rust does not catch everything at compile-time, like bounds checks and RefCell, which are checked at run-time(, and the problems due to unsafe and C interop like explained in the presentation.)
Still, IMO the comparison becomes a bit apples vs pears when bragging about how much more memory-safe Fil-C is than Rust. It would have been helpful to explain this important difference about run-time vs compile-time.
Very cool and useful anyway.
Rust has compile time checks to enforce some safety. In comparison Fil-C is a lot more memory safe, it's more comparable to being a software implementation of CHERI.
Unfortunately it comes with downsides: runtime performance, runtime enforcement, granular safety (the safety is around allocations).
It also comes with huge upsides: you can run C/C++ with little to no code changes. Imagine compiling nginx and the associated system libraries with Fil-C, the performance hit is probably acceptable and now the web server is memory safe.
Rust probably provides enough memory safety (even if it is not complete safety) in most circumstances though.
Sure, Fil-C prevents the bug from possibly being exploited, which is a huge improvement. But crashing can be a DoS attack, and if running a mission-critical system, it might not be an acceptable outcome.
I just feel the already very good presentation could have been made much better if it had put more weight on explaining these trade-offs.
The whole point of memory safety is that bugs can not be exploited. A memory safe program does not mean a crash safe program.
I gave (somewhat arbitrarily) the example that safe Rust is OK with a 64-bit pointer (on a modern PC for example) having the value made by the UTF-8 text "LAUGHING". That's 8 bytes, 8 bytes is 64 bits, it fits perfectly. Of course I point out, unsafe Rust, which is allowed to dereference pointers, must never dereference this LAUGHING pointer, it's not actually pointing at anything, but it is allowed to exist in safe Rust.
Coincidentally, in like February or so this year, a new third-party Rust type "ColdString" was introduced to their reddit by somebody I've never met - I read about it and encouraged its author. ColdString is 64 bits (on modern hardware) because inside it's a pointer, and indeed if the text you wanted to store in a ColdString was "LAUGHING" you do that exactly as I described in the December post - it's just the UTF-8 encoded text, no problem. The clever trick inside ColdString is how it can know when that 64-bit pointer really is actually a pointer for longer strings - yet also allows any 0 to 8 byte UTF-8 text string to just be encoded in the ColdString itself directly.
Now, by pointing only at memory aligned to 4 bytes, the way a 32-bit integer would be on a typical machine, the bottom two bits of the pointer are always zero. So, we ask for such 4-byte aligned pointers when allocating memory to point at, most allocators actually never give out smaller alignments anyway because they cause problems in C but we've told the allocator our requirement so even if it could it won't give out unaligned pointers.
Finally we need to re-arrange our pointer so that those two zero bits are in the right place to put a UTF-8 continuation marker. In Rust this is legal safe code, the map_addr method on a pointer is allowed to twiddle with a pointer's address bits (on typical platforms, all of them) and so long as your transform is reversible (which this one is) it will work correctly when you twiddle the pointer back and dereference that pointer in unsafe Rust.
Rust, for all its faults, at least engages with its critics. I've long faulted Rust's use of Result over exceptions, for example, but the maintainers at least acknowledge that other options exist and each has trade-offs. Not Pizlo and his fans.
To be clear, I like Fil-C. It's a practical implementation of something that should have existed a long time ago. And Pizlo is, in fact, a great programmer. It just rubs me the wrong way that he can't be content with having done excellent work --- he has to claim things that his system doesn't provide (like memory safety under data races --- minor fault, but still) and claim that other systems are worse in ways they are not (e.g. with respect to Rust having unsafe blocks).
Is genuine excellence not enough? Why must he persist in claiming a false perfection?
If considering only "safety", then Fil-C is more safe than any Rust containing unsafe blocks, no? With the usual caveats about whether an abort() is safe.
With Fil-C, the "unsafe blocks" live entirely within the compiler and runtime. With Rust, the unsafe foundation is the Rust compiler and standard library, as well as any unsafe code within your application or dependencies.
So either way you're in the same situation of relying on the correctness of the unsafe code you depend on. But there are two major differences:
- Unsafe code can be written in Rust, instead of inside the compiler. This is much easier to write and to review for correctness.
- People other than Fil are allowed to write unsafe Rust. This is what Fil's point is about, and yes, it allows you to opt-in to increasing your attack surface by trusting unsafe code written by yourself or your dependents. Rust allows the user to choose where they draw the trust boundary, and Fil-C does not.
It's true that Fil is probably better than I am at writing unsafe code. So "Fil-C is safer than Rust" is true in that sense. But Fil-C is certainly not safer than safe Rust. And sure, you can't run all the Rust code in the world if you compile with '--deny unsafe_code'; but Fil-C can't run all the C code in the world either. Unsafe Rust is rarely needed, mostly only if you want to do pointer crimes or FFI, and Fil-C doesn't support a lot of (perfectly legal) pointer crimes and FFI either.
One such example,
https://www.microsoft.com/en-us/research/publication/safe-to...
Why might I write unsafe Rust? To do something not particularly fancy. A use case covered by normal safe Fil-C. Why might I write unsafe Fil-C (compiler) code? To improve compiler or runtime performance or add new platforms, etc. Analogous to wanting to work on Rust's IR or borrow checker.
Very different use-cases.
"Blocks" are relevant as they are how you express programs, algorithms, etc., that you need in order to get something done. Lots of data structures in Rust have a little unsafe somewhere. Users will typically depend on some "specialist" crate author to write them, but it exists, and is a necessary part of practical Rust programming. The pool of unsafety is open and by necessity growing.
In Fil-C, no such specialists are needed, and the pool of unsafety is closed and fixed, no matter what programs, algorithms, data structures you use.
This is not a trivial distinction I think. I think it's easy to acknowledge, especially given its (current) performance cost.
https://x.com/filpizlo/status/2053351119095791995
And "Rust let’s you corrupt memory / Fil-C doesn’t.":
https://x.com/filpizlo/status/2079253367587737841
These are false claims. The following compiles and runs fine with Fil-C, happily corrupting memory in a way that a safer language like Rust would not allow:
#include <stdint.h>
#include <stdio.h>
struct Account {
unsigned char name[8];
uint32_t privileged;
};
void write_name(struct Account *account, size_t index, char value)
{
account->name[index] = value;
}
int main(int argc, char* argv[])
{
struct Account acct = {
.name = "foobar",
.privileged = 42,
};
write_name(&acct, 8, 1);
printf("privileged = %d\n", acct.privileged);
return 0;
}
His claims about Fil-C being safer than Rust seem to hinge on Fil-C's ability to make safe-er an entire application stack, compared to a hypothetical Rust application that links to C dependencies which (currently) can't be compiled with Fil-C. This is true as far as it goes, but it's a far cry from justifying a blanket statement like "Fil-C is safer than Rust".There are roughly two categories of Rust user:
1. People who are using Rust for application development. Most of these users write zero unsafe blocks in their careers. For these users, "memory safety" was probably not a strong reason to pick Rust, because there are a wealth of other memory-safe application development languages out there.
2. People who are using Rust for systems programming (i.e. programming under resource or environment constraints). These users may write unsafe for performance reasons, or to do things like hardware MMIO; and they're using Rust over C/C++ either for security or just because the tooling is nicer.
The first category of user is unlikely to consider C for application development in this decade; they're going to be comparing Rust against Go or Java or Node.js. Fil-C solves the security problem of memory safety, but it does not free the developer from the difficulty of having to manually write memory-safe C code; their program will just crash if they get it wrong.
The second category of user cannot use Fil-C because of its performance overhead, runtime requirements and/or lack of escape hatches for MMIO/FFI.
Where Fil-C does shine is for legacy application software written in C. Here, it's a free lunch: a way to harden the massive amount of existing software without an expensive rewrite. I would love to see distros shipping pizlonated coreutils, ffmpeg, systemd, curl, sudo, postgres, etc., anything that has a big attack surface, but does not need to be memory-unsafe.
This is a problem I care about a lot, and your work here is truly a monumental advancement in the field. Comparisons to Rust sell it short by inviting endless debate on problems largely tangential to Fil-C.
I have articulated the specific ways that Fil-C is better.
And I’ve articulated the specific ways that Rust is better. You’ve enumerated some of those reasons from your perspective, though I disagree on the details.
I like those kinds of conversations. We shouldn’t shy away from them as a community. They help us grow a shared understanding of the tech
P.S. Also fan of your work. Competition is always good.
I don’t think Rust users can be trivialized into the “two kinds” that you list, and if one of the kinds is “app developer” then I bet you there are apps where Fil-C’s value proposition is exactly right, except just the fact that Fil-C is so new and immature. Say you want to ship a native UI. Using GTK from fil-C is fantastic.
And I specifically disagree with:
> it does not free the developer from the difficulty of having to manually write memory-safe C code; their program will just crash if they get it wrong.
When I write new code in Fil-C, I just lean into the GC all the way, which makes programming in C and C++ so much nicer. I don’t ref count, I don’t use smart pointers, I don’t free and I don’t delete. It makes these languages so much nicer!
Also, Fil-C’s guarantee that it will panic on OOB or if a race goes badly means I spend basically zero time debugging memory safety issues. The reliability of the failures totally changes the dev experience for the better.
That’s subjective obviously. Some folks swear by type systems like Rust’s to catch as many issues as possible. That’s just not how I roll.
All of that said - the reason to use rust and not Fil-C is performance and memory usage. Fil-C isn’t there yet, except for maybe a small handful of cases (like BLAKE3 and xzutils, where the overhead is basically zero for some reason … we’ll probably because I did a lot of compiler opts and sometimes you get lucky and they sort of all hit)
Yeah, I like it. I also like not caring about dangling pointers, UAF, and other things, not even because of security. These are just bugs that are not fun to debug.
But I get the appeal of the freedom of C++. Especially if you deal with FFI and low level stuff, Rust either forces you to write safe wrappers (good investment long-term, but not fun to do), or just use lots of unsafe, in which case there's no benefit.
This, and there's also cargo which is convenient.
> When I write new code in Fil-C, I just lean into the GC all the way, which makes programming in C and C++ so much nicer.
Then it's no longer C/C++. More like C++/CLI maybe. And it probably gets tricky with FFI. Anyway, keep it up on making the world a safer place :)
Sure, GC's are nice which is why so many langs have them, but that removes the deterministic allocation performance which most C/C++ programmers want (and many times need). Why not just use something like Go then? You have a much richer stdlib available out of the box.
Fil-C as currently implemented does not guarantee panics on unsafe accesses due to races. You dodge the problem by using a private definition of safety under data race that permits program executions nobody would expect.
The worst that can happen in a race is that you read or write an object that would have been accessible even in the absence of races.
The thing that makes races hard to debug in C or C++ is memory corruption; that doesn’t happen in Fil-C
> The thing that makes races hard to debug in C or C++ is memory corruption; that doesn’t happen in Fil-C
is just blatantly false. It's almost as easy to corrupt memory in Fil-C as it is in C. Type confusion is still allowed. You can still do out of bounds buffer reads/writes as long as they fall within an allocation, so any intra-object stuff, as you might get in common packet or binary file parsing, is still about as unsafe as it was before.
Why?
That said, I've spent too much of my life chasing implicit control flow to accept exceptions. Heck, C++'s "noexcept" is a strong argument against exceptions all by itself.
Yes. This is why unchecked exceptions are annoying. Checked exceptions are a „failed experiment“ anyway.
A result type (or even Go‘s err) with forced error handling meaningfully increases the robustness of a program in my experience.
I haven't written any Java since it has gained those, so I suspect there might be other aspects making it awkward. And it still would lack exhaustiveness checking that e.g. Rust has.
Unchecked exceptions lead to unhandled exceptions at runtime. We've all seen screens with Java programs running with tons of exceptions in the log, or worse, that crash with unhandled exceptions. This is the result of the unchecked exceptions mess, which is why I won't use languages that use them for routine error handling for anything more than trivial programs.
Exception handling is just annoying from a syntax perspective. Also checked Exceptions have the problem that they bubble up types that a different layer shouldn’t even be aware of due to exception chaining (cause of a cause etc), unless you carefully re-throw them, which nobody did.
Lower ceremony errors are just better to deal with.
> result types pessimize code more
What does it mean? > They're arguably noisier at the source level too.
But they are more explicit, which means there will be fewer bugs in your code.Exceptions came to be during the 1990's, as the C++ARM to C++98 standardisation process was taking place.
Naturally during the early days C++ compilers lacked exceptions, as CFront was introduced without them.
Then we had the C folks that were migrating to C++.
When C++ compilers finally supported exception, compilers vendors introduced the non standard switch to disable them, so that existing code could still compile with the expected behaviour.
The standard itself does not acknowledge this as allowed.
This feature was naturally misused by the anti-exception folks, same applies to how RTTI came to be.
So noexcept is a way to be able to write code to appease both camps, while surfacing what variant was chosen by the programmer.
From my point of view allowing disabling exceptions in first place was a mistake, those folks should have moved back to C, or fixed their code.
https://smallcultfollowing.com/babysteps/blog/2024/05/02/unw...
use std::panic::catch_unwind;
fn throws_exception(){
panic!("hello");
}
fn main(){
match catch_unwind(|| {
throws_exception();
println!("Never reached");
}) {
Ok(_) => (),
Err(e) => println!(
"threw an exception: {:?}",
e.downcast_ref::<&'static str>().unwrap())
}
}
I can disable exceptions in rustc, but I can't disable the influence they have on language and library design (as detailed in the link I posted). Exceptions are the main reason you can't temporarily move something out from behind an exclusive reference, or return an error code from a Drop impl. Heck, Rust wouldn't even need destructors if it weren't for exceptions: the compiler could just tell you when you forgot to free something.> I can disable exceptions in rustc, but I can't disable the influence they have on language and library design
It's the other way around. The fact that unwinding can be (and regularly is) trivially disabled is the ultimate motivator of why panics fundamentally cannot be used for error handling, and this is what influences language and library design (ultimately demanding `Result`-based error handling).
> Exceptions are the main reason you can't temporarily move something out from behind an exclusive reference, or return an error code from a Drop impl
No, this is absolutely untrue, and I'm not sure why you think this.
> Heck, Rust wouldn't even need destructors if it weren't for exceptions: the compiler could just tell you when you forgot to free something.
No, this is also untrue, and you appear to misunderstand the purpose of destructors. Feel free to provide code if you would like to try to make a more precise argument against unwinding.
He seems to be the main developer of Fil-C: https://github.com/pizlonator/fil-c although there are other contributors.
First people use unsafe poorly-designed languages for writing critical software (already worse). Then someone invents a hack making code 4x slower and consuming 2x more memory to "fix" safety issues (much worse).
Memory Safe Context Switching - https://news.ycombinator.com/item?id=48727177 - June 2026 (30 comments)
Memory Safe Inline Assembly - https://news.ycombinator.com/item?id=48606096 - June 2026 (47 comments)
The Fil-C Optimized Calling Convention - https://news.ycombinator.com/item?id=48162876 - May 2026 (32 comments)
A simplified model of Fil-C - https://news.ycombinator.com/item?id=47810872 - April 2026 (136 comments)
Pizlix: Memory Safe Linux from Scratch - https://news.ycombinator.com/item?id=46260852 - Dec 2025 (30 comments)
Linux Sandboxes and Fil-C - https://news.ycombinator.com/item?id=46259064 - Dec 2025 (156 comments)
Ported freetype, fontconfig, harfbuzz, and graphite to Fil-C - https://news.ycombinator.com/item?id=46090009 - Nov 2025 (56 comments)
A Note on Fil-C - https://news.ycombinator.com/item?id=45842494 - Nov 2025 (210 comments)
Fil-C: A memory-safe C implementation - https://news.ycombinator.com/item?id=45842242 - Nov 2025 (1 comment)
Notes by djb on using Fil-C - https://news.ycombinator.com/item?id=45788040 - Nov 2025 (246 comments)
for more, goto https://news.ycombinator.com/item?id=45792588