Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

He claims that all syscalls are safe because they're implemented by his custom libc, but that libc then calls out to the system libc, where the system calls are unsafe. He then claims that this is unlike rust, where making a syscall is unsafe. If you stick to the rust standard library in the same way that fil-c programs stick to the fil-c standard library, then all of your rust "system calls" are safe, too.

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.



Rust doesn't runtime validate that your usage of syscalls is memory safe, while Fil-C does. For example you can call mmap in Fil-C and it is still guaranteed to be memory safe, while in Rust you can easily violate memory safety by calling mmap. This seems like an unambiguous improvement to me. This is as memory safe as it is possible to be on a system with a kernel that is not memory safe.

Adding Fil-C-like runtime checks to Rust is definitely an interesting direction.


> For example you can call mmap in Fil-C and it is still guaranteed to be memory safe, while in Rust you can easily violate memory safety by calling mmap.

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.


Fil-C has access to a memory safe API called mmap with a lot of the capabilities of the mmap system call, while Rust's safe subset does not. Rust allows you to use the mmap system call unsafely, while Fil-C does not. I feel these are both big advantages for Fil-C. You'll never have to audit a Fil-C codebase to determine whether its calls to mmap are memory safe, or indeed any other calls to any API or dependency, even those written in C.


How is Fil-C’s wrapper approach different from someone (not necessarily the Rust stdlib) implementing a safe wrapper around a particular syscall?

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.


> from someone (not necessarily the Rust stdlib) implementing a safe wrapper around a particular syscall?

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.)

[1]: https://docs.rs/nix/latest/nix/index.html


Rust is less safe because it has a feature for turning off safety, called `unsafe`. Fil-C does not have a way to turn off safety, and it enforces safety checks at runtime. That's the reasoning anyways. If you define another language Rust-Without-Any-Unsafe, then maybe that one is safer than Fil-C.


> If you define another language Rust-Without-Any-Unsafe, then maybe that one is safer than Fil-C.

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.


It should be mentioned that rust's type system has a long-standing known bug compromising memory safety, as made famous by cve-rs. It's perhaps not a big issue for most people but if you are someone that wants the assurances of #![forbid(unsafe_code)] that should probably be on your radar.


Certainly, we should build into our threat model the idea that the implementation of Rust will have bugs that compromise safety until proven otherwise (merely fixing that one aforementioned bug won't alter that calculus). And the same applies to Fil-C: we should assume it has bugs that compromise safety until proven otherwise. Until then, in practice, Rust has much more real-world use that convincingly demonstrates its ability to realistically eliminate memory safety risks.


A known bug with easily accessible proof-of-concept is quite different from a theoretical bug.


No, that's not how this works. The bug being utilized by cve-rs has such roundabout requirements that it's unlikely that anyone has ever triggered it without trying (to be clear, the Rust devs plan to fix it regardless). Meanwhile, here's a memory safety bug that Fil-C fails to catch, not because of a compiler bug, but by design: https://www.reddit.com/r/cpp/comments/1v4nw0k/filc_garbage_i...


I've encountered a few soundness holes in rust-lang in practice, but they've all been fixed before I managed to shoot myself in the foot with any of them:

• `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.


> I've encountered a few soundness holes in rust-lang in practice

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).


But the runtime cost of Fil-C means that - in most cases - your actual production application will be compiled with a standard C compiler. This applies not just to your code, but to all dependencies (due to ABI).

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.


The whole point of Fil-C is that it is fast enough to consider using in production for some applications while still guaranteeing memory safety. We already have ASAN and Valgrind and other tools for development purposes, that's not what Fil-C is targeting.


So Fil-C is competing with Go, C#, typescript and Python. If I’m writing a greenfield project where “bare metal” performance isn’t needed, why would anyone choose Fil-C over a more mature GC language?

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.


Of the group of languages presented, your argument has more merit for Go. With C# and others, there is an additional OOP argument in there, that splits into different factions.

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.


> But what is being overlooked, is the massive numerical dominance of C programmers

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)


> I haven’t seen C topping programming language popularity charts

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.


A job market specific survey: https://www.devjobsscanner.com/blog/top-8-most-demanded-prog... They combine C and C++ together and it slides in at number 6. But together they only account for 5% of the job market. Totaling all such non-managed languages together is roughly 10% and that figure hasn't changed in decades. I'd bet that figure matches up with what consultancy firms that take on a wide variety of projects see. Needless to say, "massive numerical dominance" in relation to C is something that needs many qualifications to become true. Certainly there's a lot of C code out there, and C jobs, and in some niche domains it's very dominant. But there's just such a vast amount more of JavaScript, Java, and Python across more domains.


I'm sorry, but just mentioning TIOBE has killed any credibility you might have had. I mean, seriously, look at the list.

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.


Speaking for myself, I am never hired because I know C or C++, rather specific managed languages, however if there is the need to look under the runtime hood, or have bindings, like most Python libraries actually are, then suddenly it is welcome that I know C and C++ versus most folks on the team.


> suddenly it is welcome that I know C and C++ versus most folks on the team.

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


to be fair, most programmers are at least a little bit polyglot, and if you know python and C and you're trying to solve an interview problem, python is gonna be the easier option 99% of the time, even if you're not that great at python compared to C.


> python is gonna be the easier option 99% of the time

If python/go/C# is easier to write, why choose C? Fil-C removes C’s performance and interoperability advantages.


Python is easier to write for interview questions. It generally doesn't scale well (in terms of size of team and codebase).

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.


> 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.


You could use raw C in environments with effective sandboxes, Fil-C in environments without a sandbox.

Or you could write a library that works for Fil-C and C without writing the library twice.


I am assuming that Fil-C introduces orders of magnitude less overhead than ASAN or Valgrind.

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.


I'm not sure about the speed of ASAN, it may be comparable but doesn't guarantee memory safety. It's only for catching mistakes and not secure against an adversary. Valgrind is dramatically slower.


I think it's not so much that Fil-C renders Rust completely useless, but rather it reduces the supposed validity and wild arguments to frivolously rewrite anything and everything in Rust.


Adding Fil-C-like runtime checks to Rust is definitely an interesting direction. As I mentioned upthread. It's not just the availability of the safe API that's interesting, though, but also the prohibition on using the unsafe API in the entire program and all dependencies. Which Rust could also do in theory but not yet in practice AFAIK.


> but also the prohibition on using the unsafe API in the entire program and all dependencies. Which Rust could also do in theory but not yet in practice AFAIK.

It is possible, check MaulingMonkeys' comment upthread.


Complete memory safety (much less concurrent garbage collection) isn’t really workable for things like kernel and some embedded programming contexts. Rust can work in those contexts precisely because it has an unsafe out.

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.


mmap is a great example of Fil-C providing a unique level of safety.

You can’t use mmap in a way that corrupts memory in Fil-C

Try it. :-)


Okay, let me know when I can call process_vm_writev or ptrace and have Fil-C verify safety properties on the result.


Don't forget reads and writes of /proc/self/mem! :-)


Better include Rowhammer too. Maybe Fil-C should run a test and refuse to start on any system with bad RAM or unpatched CPU errata. It could also monitor the voltage to protect against undervolting attacks. And you'll need some cosmic ray shielding too.

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.


Sure all of those are escape hatches that work against any memory safety tech.

Point is, Fil-C goes further than any other memory safety tech in terms of what it guards


Fully agreed. At some point you have to draw a line and say that the rest is the responsibility of the kernel and hardware and user, and I think Fil-C drew that line in the right place.


How does it compare to the equivalent code in Typescript, Go or C#? Those languages all have “safe” syscall wrappers too, for a subset of syscalls.


Those languages rely on a much larger pile of YOLO C/C++ code for their runtimes and standard libraries than Fil-C does.

So Fil-C is safer than those


The number of lines of raw C isn’t the only way to measure the trustworthiness of a codebase. Go, C# and friends may be bigger projects. But they’re also more mature projects. At this point, far more eyeballs have scoured their codebases looking for security bugs.

> 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.


Small nitpick but Go doesn't really have any YOLO C/C++ code in its runtime and standard library.


Yeah what I said is true of TS and C#, but not of Go.

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.


Races in Fil-C allow access to one object through a pointer to a different object if there's an attacker-controlled offset involved.

Fil-C's safety guarantees therefore fail to apply in this situation.


Nonsense.

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).


> In a race, you at worst access an object you could have loaded from whatever field you were racing on.

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.


The “bug” in your example hinges on this:

> 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


> It is not in scope of memory safety to make sure that logic not related to memory accesses works as you expected

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.


Go doesn't rely on much C/C++, not recently at least. Particularly on Linux.


If we strip away the semantic maneuvering, your argument becomes this: Rust is unsafe because it lets people who aren't you, Pizlo, write unsafe code. Safety means trusting only you with machine code.

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.


It doesn't work for a significant set of syscalls and is very very unlikely to ever work because of how the syscalls themselves work unless ofc you are willing to emulate them somehow which is a insane amount of additional compute cost.


Custom libc does not call to system libc.

“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)


The way I see it, as far as memory safety is concerned that's just framing. Both Fil-C and Rust have a boundary below which the unsafe lives. If Fil-C is only safer than rust when you don't pass the `-F unsafe_code` argument to rustc then IMO "safer" in this case is somewhat of an empty claim.


I think you should watch the linked presentation, it will show how it is "safer" than rust. Unfortunately there are performance implications, but fil-c seems fast enough to go into production for many workloads and surprisingly needs very little code changes to existing C/C++ projects which is a huge benefit.


I think Fil-C might make a lot of sense for running legacy C or C++ codebases. But for new code, it seems like it’s trying to compete with other GC languages. Take away C’s performance advantages and I don’t know why anyone would use it.

Fil-C: Combining the ergonomics of C with the performance of Python!


I like using C. It is simple, elegant, has very fast compilation times, requires no FFI needed for many libraries, is extremely stable and extremely portable, a large ecosystem, and, most importantly, I do not think the ergonomics of C are bad.

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.


Fil-C is much faster than Python

And it’s fun to write new code in.


> 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.

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"


This presentation wasn’t too bad in terms of us vs theming, but in general throughout the lifetime of the project there’s been a strong us vs them rhetoric. I think it’s working as a marketing tactic to some extent but there are better, albeit harder ways to market the thing.


Rust generally invites us vs them everywhere because it's just so ambitious and attractive (to some). Fil-C probably receives questions all the time about why they don't just do X or Y that Rust does. Maybe they end up supporting their position against that type of pressure and it looks like they're opposing Rust when all they're doing is justifying themselves.


Because by making Cyclone's ideas mainstream, which AT&T started alongside Cornell University, the C and C++ devs that so far felt safe from all those RC/GC languages, now had an actually problem as companies started paying attention and embracing the language's ideas, even extending the type system of existing RC/GC languages.

Thus anyone that identifies themselves with the programming language they work with, gets dragged into a us vs them discussion.


Arguably the most important memory safety property, i.e. bounds checking for dynamic arrays, is also not statically checked in Rust.


It's possible to do this (WUFFS does it) but it's so invasive that, well, you basically end up with WUFFS. You end up having to track every set of possible and impossible values at every location, etc. (during each program location too.) WUFFS certainly has its place and I've kind of been itching to try it out myself, but I think getting to the point where dynamic arrays can be statically bounds-checked would have highly disabled Rust.


How would Rust perform static checks on dynamic bounds? That seems like an impossible order, i.e. not a reasonable evaluation criteria for any (general-purpose) language.

(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.)


Presumably, they're talking about the possibility of explicitly replacing dynamic bounds checks in cases where it's statically provable that the access will not overrun. That doesn't necessarily mean you lose cases where you genuinely can't prove such a thing, but rather that it would be possible to opt into compiler assistance with proving it, rather than hoping LLVM will have your back. At least, that's how I'd envision such a thing for Rust. This would be similar to how you can already choose whether to invite more of the borrow checker by using references directly, or to do the checks at runtime with Rc/RefCell, etc.


Yeah, that seems like a nice thing that Rust could offer. It strikes me as a weird thing to get hung up on, though, given that the norm in compiled languages - including C - is to express your bounds such that an optimizing compiler can (but won't necessarily) eliminate them.

(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.)


> rather than hoping LLVM will have your back

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")


I merely pointed out that the statement "Rust prefers to prevent all undefined behavior statically" is misleading in the sense that Rust does not do this for all undefined behavior.


Maybe if you think of [] as offsetting a pointer rather than calling into an Index (or IndexMut) implementation. Since the return type isn't optional, a panic is the only way to avoid performing an invalid access or manufacturing an unfaithful return value. There are also optional accessors which do not panic, and unsafe/unchecked accessors.


Maybe? I think my statement is clearly correct in the way I formulated it.


ATS could probably do some of this by constructing proofs about the bounds and indices - not in the completely general case of course, but for at least some fraction of what a language without dependent types would have to defer to runtime


those are not dynamic bounds.


All "dynamic" means is that you don't know or don't prove the precise value statically. However you may know the range of possible values, or you may know properties of your algorithm that mean it can never attempt an out-of-bounds access. Sometimes you don't know any of these things, but sometimes you do.


What I was (badly) trying to express was more that given static bounds rust could also eliminate dynamic checks. So saying e.g. ATS can eliminate static checks, is kind of switching the target.


https://ats-lang.sourceforge.net/DOCUMENT/INT2PROGINATS/HTML... is the kind of thing I mean - ATS programs can include proofs that an array access will be safely within bounds, even if we don't have an exact literal subscript in the code.


Rust can not do this. But the original claim is that it is all statically checked in Rust, and this is an obvious counter example. 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.


> But the original claim is that it is all statically checked in Rust

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.


I read "Rust prefers to prevent all undefined behavior statically," to also imply that it actually does this, because otherwise the word "all" would not make sense to me in this sentence.

Dependently types languages and model checking do exist. They come with tradeoffs, but this is also true for Rust.


This isn't entirely true. Rust's indexing operator performs a dynamic check but the language and stdlib have a number of ways to access array elements without indexing, which is effectively a form of static checking. For example, the following loop is statically guaranteed to not go OOB, and will not emit any unnecessary bounds checks:

  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.


These are not terribly interesting cases though, as it would also be impossible to get an oob access in other languages either.


Yes, rust didn't invent the concept of an iterator, and is thus not the only language which offers a solution to statically avoid bounds checks. Feel free to give a more "interesting" case you believe rust wouldn't be able to handle.


Rust fails to prove basic indirection to be statically safe and instead does a run-time check.

  fn main() {
    let v = vec![1, 2, 3];
    v[5];
  }


If this was an array, that is, a built-in type with a static length, this example does give a compile-time error: https://play.rust-lang.org/?version=stable&mode=debug&editio...

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.


A: "Rust prefers to prevent all undefined behavior statically"

me: It does dynamic bounds checking.

Rustaceans: But... for XYZ ... it doesn't...

Sigh.


It's because you're not using words in the way that people use them when talking about this stuff.

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.


Rust does not do prevent all UB statically. Even if you guarantee at compile-time that all execution paths have well-defined semantics because compiler inserted run-time checks, this is not the same thing as "statically ensuring the absence of this run-time condition that would cause UB" which would be what people usually mean with "statically preventing UB". So I think it is not me who uses the works in an unusual way.

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.


These are all true? I don’t see the problem?

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.


If we can all agree that my statement was true, there is no problem.


Now it's your example that's too simple. I can't present a counter-example that wouldn't look totally different: if you avoid the indexing operator then trivially incorrect code like that becomes unrepresentable. It's like if I said destructors help you avoid memory leaks and you asked how you'd avoid a leak in a single-function program that does nothing but call `Box::leak()`.

Again: I'm not disputing the fact that the rust index operator does a dynamic bounds check.


If you agree with that Rust sometimes has to add dynamic bounds check, then you could have just agreed with my original comment.


But that's also arguably pretty boring because it's very easy to dynamically check and the cost for doing so is relatively small compared to what needs to be done for other properties.

On the other hand statically checking bounds for (dynamic and non) arrays is very very hard in the general case.


The combination of static safety gracefully degrading to runtime safety where not provable is interesting to me


This is sort of already possible in Rust when you use safe primitives (e.g., RefCell), but it doesn't cover unsafe code invoking Undefined Behavior. Placing something like Fil-C around unsafe code could prove to be interesting, especially since current approaches rely on interpretation (Miri, Soteria Rust) rather than translation.


In Rust, there are safe syscall wrappers, and then there's the unsafe general `syscall` function: https://docs.rs/libc/latest/libc/fn.syscall.html.

If I'm understanding correctly, it's this general syscall function that's safe in Fil-C.


indeed, the ideas could be used more widely.

fil-c runs on linux.

what if linux ran on fil-c?


Do you want your kernel to be 2x slower and use 4x as much memory?


A computer spends the vast majority of its time and memory in userspace, so this tradeoff isn't as bad as it sounds.


The benefit also isn’t as big as you might expect. Most of Linux’s recently found security vulnerabilities were due to ToC/ToU bugs. Fil-C would not magically fix these problems.

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.


a fun exercise is to ask an LLM how it might be applied.


You can run your Fil–C userland on provably–secure seL4.

(Left as an exercise for the reader)


If you’re using SeL4, userland processes are already strictly sandboxed. There’s still some benefit to Fil-C, since the added memory safety would make it much more difficult to take over a process. But the blast radius of a compromised program in SeL4 is much smaller because of the capability model.


I was under the impression that fil-c's memory management (gc?) wouldn't work on a kernel? Be sweet if you could, ofc


This is a myth that seems to never die. OS kernels can and have been written in GC languages. Watch the video maybe? The whole presentation was running on a distro fully compiled with FillC.


The video has a slide which says,

> 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?


Ok, I stand corrected: only the kernel userland was compiled 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


what if fil-c could draw pelicans?


also: why not use fil-c to improve cython, and the ffi.


the only reason to use cython (in my experience) is to write quick numpy extensions without a tonne of extra baggage from having a complete compiled extension in C/C++/Rust. Often, you explicitly want to avoid the compiler offering you any kinds of additional checking because you want to get maximum throughput. Most of the functions I’ve written in cython explicitly opt out of bounds checking etc since the memory access is sequential and bounded by construction and very obvious when you mess things up by writing simple unit tests. Given that, it doesn’t seem useful to me to add any kind of memory safety as additional overhead. YMMV I mostly do scientific computing and signal processing.


If you want a slow, garbage collected language in the Python ecosystem, why not just write Python?


you would. python and ffi would have runtime sanity checks a la fil-c. better error messages too. ask your favorite LLM how it would look.


Both Rust and Fil-C have unsafe blocks, but in Fil-C latter system, only Pizlo gets to write them. Why am I not filled with confidence?


Fil-C has no unsafe blocks


Is the runtime compiled by the Fil-C compiler, or is it a single huge unsafe block? I honestly don't know the answer.


But it still defers compile-time errors to run-time. Not safe enough for me. I rarely have 100% coverage




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: