So many mentions of CHERI - both in this post and in the linked CACM article. I doubt CHERI will be the future considering how long it's been around for and how few actually successes have come out of it.
Also, hilariously, Fil-C is faster than CHERI today (the fastest CHERI silicon available today will be slower than Fil-C running on my laptop, probably by an order of magnitude). And Fil-C is safer.
Which sort of brings up another issue - this could be a case where Google is trying to angle for regulations that support their favorite strategy while excluding competition they don't like.
What is the distinction between this approach and Address Sanitizer https://clang.llvm.org/docs/AddressSanitizer.html ? If I understand correctly, Fil-C is a modified version of LLVM. Is your metadata more lightweight, catches more bugs? Could it become a pass in regular LLVM?
Fil-C is memory safe. Asan isn't.
For example, asan will totally let you access out of bounds of an object. Say buf[index] is an out-of-bounds access that ends up inside of another object. Asan will allow that. Fil-C won't. That's kind of a key spacial safety protection.
Asan is for finding bugs, at best. Fil-C is for actually making your code memory safe.
Sorry, I’m not a C specialist. What are Fil-C and CHERI? eg. Safe subsets of C, static analysis tools, C toolsets to ensure memory safety?
CHERI is a hardware architecture and instruction set to add safety-related capabilities to processors. See https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/ In this context a capability means a way to track and enforce which memory area a pointer can point into. Typically this has to be coupled with a compiler which will initialize the capability for each pointer.
Fil-C seems to be a C variant that adds capabilities and garbage collection. See https://github.com/pizlonator/llvm-project-deluge/blob/delug...
> Fil-C is currently 1.5x slower than normal C in good cases, and about 4x slower in the worst cases. I'm actively working on performance optimizations for Fil-C, so that 4x number will go down.
I am pretty sure you cannot go much lower than 1.2 here on the best cases. In contrast, CHERI on good hardware will easily be as close to current performance as possible.
Cheri hardware is going to be more than 1.2x slower than the fastest non Cheri hardware.
Do you say that CHERI will be "as close to current performance as possible" with the same energy consumption?
It isn't CHERI, however Solaris SPARC ADI has more than proven its usefulness, it isn't more widely deployed due to the reasons we all know.
>it isn't more widely deployed due to the reasons we all know.
Google doesn't come out with anything useful. Could you elaborate?
Not really buying your thesis here: Attempts to retrofit safely to C have been around a lot longer than Cheri; fil-c is just the latest and there's no obvious reason why it should be more successful.
The speed comparison with a laptop is just disingenuous. Is a device with Cheri integrated slower than one of the same class without?
> Not really buying your thesis here: Attempts to retrofit safely to C have been around a lot longer than Cheri; fil-c is just the latest and there's no obvious reason why it should be more successful.
Here's the difference with Fil-C: It's totally memory safe and fanatically compatible with C/C++, no B.S. The other approaches tend to either create a different language (not compatible), or tend to create a superset of C/C++ with a safe subset (not totally memory safe, only compatible in the sense that unsafe code continues to be unsafe), or just don't fully square the circle (for example SoftBound could have gotten to full compatibility and safety, but the implementation just didn't).
> The speed comparison with a laptop is just disingenuous. Is a device with Cheri integrated slower than one of the same class without?
Not disingenuous at all.
The issue is that:
- high volume silicon tends to outperform low volume silicon. Fil-C runs on x86_64 (and could run on ARM64 if I had the resources to regularly test on it). So, Fil-C runs on the high volume stuff that gets all of the best optimizations.
- silicon optimization is best done incrementally on top of an already fast chip, where the software being optimized for already runs on that chip. Then it's a matter of collecting traces on that software and tuning, rather than having to think through how to optimize a fast chip from first principles. CHERI means a new register file and new instructions so it doesn't lend itself well to the incremental optimization.
So, I suspect Fil-C will always be faster than CHERI. This is especially true if you consider that there are lots of possible optimizations to Fil-C that I just haven't had a chance to land yet.
> Here's the difference with Fil-C: It's totally memory safe and fanatically compatible with C/C++, no B.S. The other approaches tend to either create a different language (not compatible), or tend to create a superset of C/C++ with a safe subset (not totally memory safe, only compatible in the sense that unsafe code continues to be unsafe), or just don't fully square the circle (for example SoftBound could have gotten to full compatibility and safety, but the implementation just didn't).
That's exactly the kind of thing that the boosters of all those previous efforts said. But somehow it never quite worked out.
Also, it always had a material performance impact. People write C++, and to a lesser extent C, because they really, really care about performance. If they didn’t care about performance there are easier languages to use.
Talking about performance impact is missing the bigger picture of how languages become performant. "Really really care about performance" describes some C/C++ programmers, but definitely not all of them. Finally, Fil-C is already faster than a lot of other safe languages (definitely faster than TypeScript, yet lots of stuff ships in TypeScript).
Language implementations get faster over time and young ones tend to be slow. The Fil-C implementation is young. So were all of the previous attempts at memory-safe C - usually an implementation that had years of at most a few person years of investment (because it was done in an academic setting). Young implementations tend to be slow because the optimization investment hasn't happened in anger. So, "past academic attempts were slow" is not a great reason to avoid investigating memory safe C.
Performance focus is not the reason why all of the world's C/C++ code gets written. Maybe that's even a minority reason. Lots of stuff uses C/C++ because of reasons like:
- It started out in C/C++ so it continues to be in C/C++. So many huge projects are in this boat.
- You're critically relying on a library whose only bindings are in C/C++, or the C/C++ bindings are the most mature, or the most easy to use.
- You're doing low-level systems stuff, and having pointers that you can pass to syscalls is a core part of your logic.
- You want to play nice with the dynamic linking situation on the OS you're targeting. (C/C++ get dynamic linking right in a way other languages don't.)
I'd guess less than half of the C/C++ code that's being written today is being written because the programmer was thinking "oh man, this'll be too slow in any other language".
Finally, Fil-C is already faster than a lot of memory safe languages. It's just not as fast as Yolo-C, but I don't think you can safely bet that this will be true forever.
> That's exactly the kind of thing that the boosters of all those previous efforts said. But somehow it never quite worked out.
No, they really didn't. Let's review some of the big cases.
- SafeC: not based on a mainstream C compiler, so can't handle gcc/clang extensions (Fil-C can). Had no story for threads or shared memory (Fil-C does). Hence, not really compatible.
- CCured: incompatible (cannot compile C code with it without making changes, or running their tool that tries to automate the changes - but even then, common C idioms like unions don't quite work). Didn't use a major C compiler,
- SoftBound: not totally memory safe (no attempt to provide safety for linking or function calls). But at least it's highly compatible.
I can list more examples. Fil-C is the first to get both compatibility and safety right.
> Fil-C is the first to get both compatibility and safety right.
Has any impartial third party reached that conclusion? Because honestly the way I remember it everyone says this kind of thing when it's their own project, a lot of the people behind these previous efforts were just as confident as you are.
Not in any official capacity but it’s been looked at by other C compiler experts, other programming language experts, GC experts, and security experts. Folks who have looked at it deeply agree with those claims. And I hope they would have told me if they didn’t believe anything about my claims!
> That's exactly the kind of thing that the boosters of all those previous efforts said
I don't think this is true. - D, Swift, Rust, Zig: different languages, and while they do have FFI, using it means you're only as safe as your C code - CHERI: requires hardware support to be practical - Checked C, CCured, ?SAFECode IIRC?: too expensive - AddrSan@runtime, ARM MTE, SoftBound: mitigations with too many holes
I don't know of many (to be honest, can't think of any) other serious attempts at making a system that tries to cover all three of
A) lets you write normal C
B) covers all the gaps
C) doesn't kill performance
Well I've got 2/3 so far!
Maybe 3/3 depending on your workload and definition of "killing performance". It's less than 2x slower for some stuff.
The good news is Fil-C is getting faster all the time, and there are still so many obvious optimizations that I haven't gotten around to.
I don't get it, what's the catch? Why isn't everyone using Fil-C immediately everywhere?
Lots of reasons.
Here's one: even just switching from gcc or msvc to clang, in projects that really want to, takes years.
Here's another one: the Fil-C compiler is really young, so it almost certainly still has bugs. Those compilers that folks actually use in anger tend to get qualified on ~billions of lines of code before anyone other than the compiler devs touches them. The Fil-C compiler is too young to have that level of qualification.
So "immediately everywhere" isn't going to happen. At best it'll be "over a period of time and incrementally".
Awful performance. Usually 2x worse than C and 4x worse in the worst case. Given the comment by Fil-C's creator minimizing the performance issue [0], I wouldn't get my hopes up.
[0] https://news.ycombinator.com/item?id=43190938
I guess you missed the point of that post.
I’ll summarize: language implementations get faster over time. Young ones tend to be slow. Fil-C is a young implementation that still has lots of unoptimized things. Also, Fil-C being 2x slower than C means it’s already faster than many safe languages. And, for a lot of C use cases perf doesn’t matter as much as the hype suggests.
The fact that young implementations are slow is something that’s worth understanding even if you don’t care about fil-C. It suggests, for example, that if someone invents a new language and their initial implementation is slow, then you can’t use that fact to assume that it’ll be slow forever. I think that’s generally a useful lesson.
I care about performance a lot and Fil-C has gotten about 100x faster since the first prototype. It’ll keep getting faster.
It's not widely known, and it seems to be still in the research stage. A lot of things in this area never really get out of that. It did not happen for MPX. Many distributions build binaries for SHSTK, but I doubt anyone is enabling it by default. Even Address Sanitizer still relies on wrappers on the side, does not provide ABI stability, and is generally not considered ready for production binaries. I don't think there's a mainstream distribution where linking with -fsanitizer=address automatically gives you the Address Sanitizer version of system libraries. (Wouldn't that be nice?)
Getting these things to mass deployment, ticking all those little boxes, is a lot of effort. Porting a distribution to a new CPU architecture is likely easier, especially after the early stages (toolchain bringup).
Apart from that, there could be technical issues with the proposed approach. Perhaps the memory overhead? Or it might turn out that the desired performance characteristics basically require a JIT that specializes code so that typed pointers can be used where the types are known to be correct.
Pretty much everything you're saying is true.
> It's not widely known, and it seems to be still in the research stage.
Yes on both counts.
> A lot of things in this area never really get out of that.
I hope that doesn't happen to Fil-C, but it could!
> Getting these things to mass deployment, ticking all those little boxes, is a lot of effort.
100%
I think this is one area where I'm trying to make Fil-C different than what came before it. I'm trying to tick all those little boxes. It's a lot of work!
> Porting a distribution to a new CPU architecture is likely easier, especially after the early stages (toolchain bringup).
Not sure about this. It might be true today because Fil-C hasn't yet ticked all the boxes, but the aim is definitely to be close to the cost of porting to a new CPU. It's already like that for a lot of code.
> Perhaps the memory overhead?
Heh yeah. The invisicaps cost memory. And GC costs memory.
> Or it might turn out that the desired performance characteristics basically require a JIT that specializes code so that typed pointers can be used where the types are known to be correct.
I've thought about how a JIT might help. I don't think it would. (Most of my compiler experience is writing JITs and I wrote JavaScriptCore's JITs, so I'm biased towards seeing JIT opt opportunities - and I don't see any in Fil-C right now.)
Solaris SPARC ADI.
https://docs.oracle.com/en/operating-systems/solaris/oracle-...
I've been really impressed with what you're doing with Fil-C, but:
> Here's the difference with Fil-C: It's totally memory safe and fanatically compatible with C/C++, no B.S.
Is this true on both counts? If I'm reading your docs right, you're essentially adding hidden capabilities to pointers. This is a great technique that gives you almost perfect machine-level compatibility by default, but it comes with the standard caveats:
1. Your type safety/confusion guards are essentially tied to pointer "color," and colors are finite. In other words, in a sufficiently large program, an attacker can still perform type confusion by finding types with overlapping colors. Not an issue in small codebases, but maybe in browser- or kernel-sized ones.
2. In terms of compatibility, I'm pretty sure this doesn't allow a handful of pretty common pointer-integer roundtrip operations, at least not without having the user/programmer reassign the capability to the pointer that's been created out of "thin air." You could argue correctly that this is a bad thing that programmers shouldn't be doing, but it's well-defined and common enough IME.
(You also cited my blog's writeup of `totally_safe_transmute` as an example of something that Fil-C would prevent, but I'm not sure I agree: the I/O effect in that example means that the program could thwart the runtime checks themselves. Of course, it's fair to say that /proc/self/mem is a stunt anyways.)
My type safety and confusion guards have nothing to do with colors of any kind. There are no finite colors to run out of.
Fil-C totally allows pointer to integer round tripping in many cases, if the compiler can see it’s safe.
I’m citing unsafe transmute as something that Fil-C doesn’t prevent. It’s not something that memory safety prevents.
> My type safety and confusion guards have nothing to do with colors of any kind. There are no finite colors to run out of.
I'm having trouble seeing where the type confusion protection properties come from, then. I read through your earlier (I think?) design that involved isoheaps and it made sense in that context, but the newer stuff (in `gimso_semantics.md` and `invisicap.txt`) seems to mostly be around bounds checking instead. Apologies if I'm missing something obvious.
> I’m citing unsafe transmute as something that Fil-C doesn’t prevent. It’s not something that memory safety prevents.
I think the phrasing is confusing, because this is what the manifesto says:
> No program accepted by the Fil-C compiler can possibly go on to escape out of the Fil-C type system.
This to me suggests that Fil-C's type system detects I/O effects, but I take it that wasn't the intended suggestion.
Here’s a write up that goes into more detail: https://github.com/pizlonator/llvm-project-deluge/blob/delug...
I read that, but I'm not seeing where the type is encoded in the capability.
Short answer: if you know how CHERI and SoftBound do it, then Fil-C is basically like that.
Long answer: let's assume 64-bit (8 byte pointers) without loss of generality. Each capability knows, for each 8 bytes in its allocation, whether those 8 bytes are a pointer, and if so, what that pointer's capability is.
Example:
This will allocate 64 bytes. p's capability will know, for each of the 8 8-byte slots, if that slot is a pointer and if so, what it's capability is. Since you just allocated the object, none of them have capabilities.
Then if you do:
Then p's capability will know that at offset 8, there is a pointer, and it will know that the capability is whatever came out of the malloc.
Hence, each capability is dynamically tracking where the pointers are. So it's not a static type but rather something that can change over time.
There's a bunch of engineering that goes into this being safe under races (it is) and for supporting pointer atomics (they just work).
BTW I wrote another doc to try to explain what's happening. Hope this helps.
https://github.com/pizlonator/llvm-project-deluge/blob/delug...
Thank you, I found this very helpful!
This is helpful as well because it at least alludes to the data structures: https://github.com/pizlonator/llvm-project-deluge/blob/delug...
One more thing: I assume the memory allocator has a somewhat efficient way to recover the pointer to the start of the allocation from a pointer inside it, for interoperability with the legacy C world. For recompiled code, this is not used because the capability is either tracked in registers, or loaded from the shadow memory.
The GC does marking using the capability pointer, not the pointer's integer value. So, there's no need for a general "hey GC, what object does this point into and where does it start" query.
The capability is the start of the allocation most of the time. It might have a bit in it saying that it's an aligned allocation (indicating alignment greater than 16 bytes), in which case there's an alignment hole between the start of what the GC thinks is the allocation and where the capability starts. Also capabilities might be global, meaning that there is no need to mark them. So, to mark a capability (which the runtime calls an "object"), the algorithm is something like:
I read the Fil-C overview, and I was confused by one thing: how does Fil-C handle integer-to-pointer conversions? Rust has the new strict provenance API that is somewhat explicitly designed to avoid a need to materialize a pointer capability from just an integer, but C and C++ have no such thing. So if the code does:
Does this fail unconditionally? Or is there some trick by which it can succeed if p is valid? And, if the latter is the case, then how is memory safety preserved?
edit: I found zptrtable and this construct:
https://github.com/pizlonator/pizlonated-quickjs/commit/258a...
The latter seems to indicate that:
is at least a semi-reliable way to increment p by one. I guess this is a decent way to look like C and to keep a widely-used pattern functional. Rust’s with_addr seems like a more explicit and less magical way to accomplish the same thing. If Fil-C really takes off, would you want to add something like with_addr? Is allowing the pair of conversions on the same line of code something that can be fully specified and can be guaranteed to compile correctly such that it never accidentally produces a pointer with no capability?
Your deref function will fail, yeah.
The pair of conversions is guaranreed to always produce a pointer with a capability. That’s how I implemented it and it’s low-tech enough that it could be specified.
How far can the pair of conversions be pushed? Will this work:
Does it matter if f is inline?
Could someone implement Rust’s with_addr as:
FWIW, I kind of like zptrtable, and I think Fil-C sounds awesome. And I’m impressed that you were able to port large code bases with as few changes as it seems to have taken.
Your first example will hilariously work if `f` is inline and simple enough and optimizations are turned on. I'm not sure I like that, so I might change that. I'd like to only guarantee the you get a capability in cases where that guarantee holds regardless of optimization (and I could achieve that with some more compiler hacking).
Not sure about the semantics of with_addr. But note that you can do this in Fil-C:
I have a helper like that called `zmkptr` (it's just an inline function that does exactly the above).
with_addr is basically that, but with a name and some documentation:
https://doc.rust-lang.org/std/primitive.pointer.html#method....
As I understand it, Rust added this in part for experiments with CHERI but mostly for miri.
Interestingly, the implementation of with_addr is very similar to your code.
How do you handle cases where there are multiple possible sources of the capability? For example:
I’m not sure I would allow this into any code I maintain, but still. There’s also the classic xor-list, and someone has probably done it like:
And this needs to either result in a compiler error or generate some kind of code.
Rust’s with_addr wins points for being explicit and unambiguous. It obviously loses points for not being C. And Rust benefits here from all of this being in the standard library and from some of the widely-available tooling (miri) getting mad if code doesn’t use it. I can imagine a future Fil-Rust project doing essentially the same thing as Fil-C except starting with Rust code. It might be interesting to see how the GC part would interact with the rest of the language.
My compiler analysis says that if you have two possible pointers that a capability might come from, like in your first example, then you get no capability at all. I think that's a better semantics than picking some capability at random.
If you want to be explicit about where the capability comes from, use `zmkptr`.
> Attempts to retrofit safely to C have been around a lot longer than Cheri; fil-c is just the latest and there's no obvious reason why it should be more successful.
It is true that memory-safe C compilers have existed for decades and have seen minimal adoption.
However, improvements to clang/llvm could yield wider impact and benefit than previous efforts, since they may be supported in a widely used C toolchain.
-fbounds-safety is another change that may see more adoption if it makes it into mainline clang/llvm
https://clang.llvm.org/docs/BoundsSafetyAdoptionGuide.html
> Fil-C is faster than CHERI
Except for those GC pauses...
No GC pauses. Fil-C uses a concurrent GC.
For CHERI to be fully safe, it basically needs a GC. They just call it something else. They need it to clean up capabilities to things that were freed, which is the same thing that Fil-C uses GC for.