News for hackers. Stuff that matters.
Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Wednesday February 26, 2025 @06:23PM (#43189447)
> 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.

Re:Securing tomorrow's software: the need for memory safety ... by lmm on Wednesday February 26, 2025 @07:33PM (#43190001)
> 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.

Re:Securing tomorrow's software: the need for memory safety ... by jandrewrogers on Wednesday February 26, 2025 @09:53PM (#43190820)
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.
Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Wednesday February 26, 2025 @10:14PM (#43190938)
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.

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Wednesday February 26, 2025 @10:00PM (#43190862)
> 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.

Re:Securing tomorrow's software: the need for memory safety ... by lmm on Wednesday February 26, 2025 @11:58PM (#43191494)
> 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.

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @12:26AM (#43191615)
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!
Re:Securing tomorrow's software: the need for memory safety ... by achierius on Wednesday February 26, 2025 @10:08PM (#43190904)
> 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

Re:Securing tomorrow's software: the need for memory safety ... by Nathanba on Wednesday February 26, 2025 @11:25PM (#43191353)
I don't get it, what's the catch? Why isn't everyone using Fil-C immediately everywhere?
Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @11:13AM (#43195709)
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".

Re:Securing tomorrow's software: the need for memory safety ... by throwaway4655 on Thursday February 27, 2025 @06:01PM (#43199543)
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

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @06:21PM (#43199701)
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.

Re:Securing tomorrow's software: the need for memory safety ... by fweimer on Saturday March 01, 2025 @03:46PM (#43223518)
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.

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Sunday March 02, 2025 @05:48PM (#43236097)
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.)

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Wednesday February 26, 2025 @10:16PM (#43190943)
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.

Re:Securing tomorrow's software: the need for memory safety ... by pjmlp on Thursday February 27, 2025 @04:54PM (#43198944)
Re:Securing tomorrow's software: the need for memory safety ... by woodruffw on Wednesday February 26, 2025 @11:17PM (#43191307)
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.)

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Wednesday February 26, 2025 @11:39PM (#43191419)
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 nothing that memory safety prevents.

Re:Securing tomorrow's software: the need for memory safety ... by woodruffw on Thursday February 27, 2025 @01:02AM (#43191760)
> 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.

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @07:58AM (#43193976)
Here’s a write up that goes into more detail: https://github.com/pizlonator/llvm-project-deluge/blob/delug...
Re:Securing tomorrow's software: the need for memory safety ... by woodruffw on Thursday February 27, 2025 @11:59AM (#43196170)
I read that, but I'm not seeing where the type is encoded in the capability.
Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @12:19PM (#43196360)
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:

    char* p = malloc(64);
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:

    *(int**)(p + 8) = malloc(sizeof(int));
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).

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @09:02PM (#43200837)
BTW I wrote another doc to try to explain what's happening. Hope this helps.

https://github.com/pizlonator/llvm-project-deluge/blob/delug...

Re:Securing tomorrow's software: the need for memory safety ... by woodruffw on Thursday February 27, 2025 @10:41PM (#43201466)
Thank you, I found this very helpful!
Re:Securing tomorrow's software: the need for memory safety ... by fweimer on Saturday March 01, 2025 @02:48PM (#43222941)
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.

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Monday March 03, 2025 @09:52AM (#43242279)
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:

    if (object->is_global)
        return;
    void* thing_to_mark;
    if (UNLIKELY(object->is_aligned))
        thing_to_mark = object & -object->alignment;
    else
        thing_to_mark = object;
    mark(thing_to_mark);
Re:Securing tomorrow's software: the need for memory safety ... by amluto on Thursday February 27, 2025 @03:53AM (#43192520)
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:

    int deref(uintptr_t p)
    {
        return *(int*)p;
    }
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:

    (int*)((uintptr_t)p + 1)
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?
Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @09:35AM (#43194720)
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.

Re:Securing tomorrow's software: the need for memory safety ... by amluto on Thursday February 27, 2025 @11:18AM (#43195758)
How far can the pair of conversions be pushed? Will this work:

    (int*)(f((uintptr_t)p))
Does it matter if f is inline?

Could someone implement Rust’s with_addr as:

    (int*)((uintptr_t)p, addr)
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.
Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @11:28AM (#43195832)
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:

    char* p = ...;
    uintptr_t i = ...;
    p -= (uintptr_t)p; // now p is NULL but still has its original capability
    p += i; // now p points to whatever i's address was, but has p's original capability
I have a helper like that called `zmkptr` (it's just an inline function that does exactly the above).
Re:Securing tomorrow's software: the need for memory safety ... by amluto on Thursday February 27, 2025 @01:19PM (#43196917)
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:

    int *a = …, *b = …;
    (int*)(((uintptr_t)a + (uintptr_t)b) << 2)
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:

    struct node { struct node *p; };
    
    struct node *next(struct node *here, struct node *prev)
    {
        return (struct node *)((uintptr_t)here ^ (uintptr_t)prev);
    }
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.

Re:Securing tomorrow's software: the need for memory safety ... by pizlonator on Thursday February 27, 2025 @01:34PM (#43197058)
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`.