introduce an actually memory safe (unlike Rust) compilation mode inspired by Fil-C #36237

Open
opened 2026-07-20 04:58:59 +02:00 by andrewrk · 16 comments
andrewrk commented 2026-07-20 04:58:59 +02:00
Owner

Based on prior art: Fil-C

Currently Zig has:

pub const Optimize = enum {
    /// Safety checks enabled. Optimize for bug detection, accurate debug info,
    /// and compilation speed (in that order).
    debug,
    /// Safety checks enabled. Optimize for runtime performance.
    safe,
    /// Safety checks disabled. Optimize for runtime performance.
    fast,
    /// Safety checks disabled. Optimize for machine code size, then performance.
    small,
};

This proposal is to keep the existing optimization modes, while introducing an additional ABI ("fil") alongside musl, gnu, msvc, etc.

Related: RFC/Proposal: Turning Zig target triples into quadruples

TODO: contact Fil and ask if he wants make Fil-C ABI stable, versioned, or otherwise. Zig project would prefer to adhere to a standard, if possible.

This ABI has limitations:

  • incompatible with the other ABIs. All objects must be compiled with fil ABI or linking cannot succeed. Zig linkers must identify incompatible objects and refuse to link.
  • mainly it is expected to combine this ABI with Optimize.safe or Optimize.debug. However, the other modes can also work, but for example invisicaps would still be implemented (i.e. runtime pointer provenance checks) because that is needed by the ABI.
  • currently it is only defined for x86_64-linux, however, support for other targets could be expanded over time.

When using this ABI, Zig compiler would implement the same techniques from the Fil-C project. That is, invisicaps combined with a tight coupling with the target OS in order to wrap all syscalls. No dependency on the Fil-C project; an alternative implementation.

One opportunity to diverge here is garbage collection. Fil pointed out to me that GC (see FUGC) is just one way to implement this technique. Another one would be automatic insertion of unique type ids into invisicaps metadata, ensuring that if UAF does occur, type confusion is still prevented. Unique type IDs are already a necessary ingredient for the accepted proposal add safety checks for pointer casting.

The main challenge to solve is that Zig needs to intercept all syscalls (and in fact all inline assembly), just like Fil-C currently does today. Allocator interface is OK, but mmap has to be replaced with wrapper code. I don't think this requires any language changes however. I think it can be done via compiler and standard library changes alone.

By doing this, Zig can offer its users an option to compile their projects, including entire tree of C/C++ dependencies, into an executable that is fully memory safe, with no escape hatch, at a ~1-6x performance penalty (depending on prevalence of pointer chasing).

Once this is implemented then, in a twist of fate, Zig will become one of the only toolchains capable of producing actually memory safe executables, especially when you consider that real world applications tend to have C/C++ dependencies and, in the case of Rust, additional use of unsafe beyond FFI.

Based on prior art: [Fil-C](https://fil-c.org/) Currently Zig has: ```zig pub const Optimize = enum { /// Safety checks enabled. Optimize for bug detection, accurate debug info, /// and compilation speed (in that order). debug, /// Safety checks enabled. Optimize for runtime performance. safe, /// Safety checks disabled. Optimize for runtime performance. fast, /// Safety checks disabled. Optimize for machine code size, then performance. small, }; ``` This proposal is to **keep the existing optimization modes**, while introducing an additional ABI ("fil") alongside musl, gnu, msvc, etc. Related: [RFC/Proposal: Turning Zig target triples into quadruples](https://github.com/ziglang/zig/issues/20690) TODO: contact Fil and ask if he wants make Fil-C ABI stable, versioned, or otherwise. Zig project would prefer to adhere to a standard, if possible. This ABI has limitations: - incompatible with the other ABIs. All objects must be compiled with fil ABI or linking cannot succeed. Zig linkers must identify incompatible objects and refuse to link. - mainly it is expected to combine this ABI with `Optimize.safe` or `Optimize.debug`. However, the other modes can also work, but for example invisicaps would still be implemented (i.e. runtime pointer provenance checks) because that is needed by the ABI. - currently it is only defined for x86_64-linux, however, support for other targets could be expanded over time. When using this ABI, Zig compiler would implement the same techniques from the Fil-C project. That is, [invisicaps](https://fil-c.org/invisicaps) combined with a tight coupling with the target OS in order to wrap all syscalls. No dependency on the Fil-C project; an alternative implementation. One opportunity to diverge here is garbage collection. Fil pointed out to me that GC (see [FUGC](https://fil-c.org/fugc)) is just one way to implement this technique. Another one would be automatic insertion of unique type ids into invisicaps metadata, ensuring that if UAF does occur, type confusion is still prevented. Unique type IDs are already a necessary ingredient for the accepted proposal [add safety checks for pointer casting](https://github.com/ziglang/zig/issues/2414). The main challenge to solve is that Zig needs to intercept all syscalls (and in fact all inline assembly), just like Fil-C currently does today. Allocator interface is OK, but `mmap` has to be replaced with wrapper code. I don't think this requires any language changes however. I think it can be done via compiler and standard library changes alone. By doing this, Zig can offer its users an option to compile their projects, *including entire tree of C/C++ dependencies*, into an executable that is fully memory safe, **with no escape hatch**, at a ~1-6x performance penalty (depending on prevalence of pointer chasing). Once this is implemented then, in a twist of fate, Zig will become one of the only toolchains capable of producing *actually memory safe* executables, especially when you consider that real world applications tend to have C/C++ dependencies and, in the case of Rust, additional use of `unsafe` beyond FFI.
andrewrk added this to the Upcoming milestone 2026-07-20 04:58:59 +02:00
Owner

The aux word of an object that has no pointers is simply NULL. But if an object had any pointers stored into it, an aux allocation is allocated, which has the same size as the object's payload. The aux word then points at the aux allocation. If we ignore atomic pointers for now, all pointers at rest in the object have their lower in the aux allocation and their intval in the object payload.

This seems fundamentally at odds with Zig's philosophy on allocations.

Same here:

Atomic boxes are 16-byte, 16-byte-aligned allocations that contain a flight pointer that we store using 128-bit atomics. Or, if the system does not support double-CAS, we could use 64-bit atomics on the atomic box pointer itself and allocate a new atomic box every time an atomic mutation happens (luckily both X86_64 and ARM64 have 128-bit atomics, so that's not necessary).

(Besides being very inefficient for basically every other architecture, and tricky to implement in an async-signal-safe manner.)

> The aux word of an object that has no pointers is simply NULL. But if an object had any pointers stored into it, an aux allocation is allocated, which has the same size as the object's payload. The aux word then points at the aux allocation. If we ignore atomic pointers for now, all pointers at rest in the object have their lower in the aux allocation and their intval in the object payload. This seems fundamentally at odds with Zig's philosophy on allocations. Same here: > Atomic boxes are 16-byte, 16-byte-aligned allocations that contain a flight pointer that we store using 128-bit atomics. Or, if the system does not support double-CAS, we could use 64-bit atomics on the atomic box pointer itself and allocate a new atomic box every time an atomic mutation happens (luckily both X86_64 and ARM64 have 128-bit atomics, so that's not necessary). (Besides being very inefficient for basically every other architecture, and tricky to implement in an async-signal-safe manner.)
Author
Owner

This seems fundamentally at odds with Zig's philosophy on allocations.

Can you be more specific?

> This seems fundamentally at odds with Zig's philosophy on allocations. Can you be more specific?
Owner

Well unless I'm completely misinterpreting the linked invisicaps page:

  • Both of the quoted paragraphs imply hidden allocations for the scheme to work.
  • The allocations occur on language constructs that wouldn't allocate even in a managed language like C#, let alone a language like Zig.
  • Failure of those allocations cannot be handled gracefully.
Well unless I'm completely misinterpreting the linked invisicaps page: * Both of the quoted paragraphs imply hidden allocations for the scheme to work. * The allocations occur on language constructs that wouldn't allocate even in a managed language like C#, let alone a language like Zig. * Failure of those allocations cannot be handled gracefully.

I previously wrote a few exploratory spec notes around capability-bounded allocators and temporal memory-safety ideas, including task-scoped arenas, in zig

one question was whether the proposed non-GC approach might also need an allocation generation or object ID to catch same-type address reuse. The notes are still rough, but they may overlap with this proposal. Looking forward to seeing where this goes, Andrew.

I previously wrote a few exploratory spec notes around capability-bounded allocators and temporal memory-safety ideas, including task-scoped arenas, in zig one question was whether the proposed non-GC approach might also need an allocation generation or object ID to catch same-type address reuse. The notes are still rough, but they may overlap with this proposal. Looking forward to seeing where this goes, Andrew.

Id like a clarification on how this is safer than Rust?

Id like a clarification on how this is safer than Rust?
cztomsik commented 2026-07-20 17:42:12 +02:00

Do I understand it correctly that this is mostly going to affect pointers but if I am using indices / array-access then this could still have next-to-zero overhead?

EDIT: One extra question - is there a chance that this could be enabled ad-hoc with @setRuntimeSafety()? No idea if it's feasible but is that the direction? Or is this rather supposed to be all-or-nothing flag?

Do I understand it correctly that this is mostly going to affect pointers but if I am using indices / array-access then this could still have next-to-zero overhead? EDIT: One extra question - is there a chance that this could be enabled ad-hoc with @setRuntimeSafety()? No idea if it's feasible but is that the direction? Or is this rather supposed to be all-or-nothing flag?
Contributor

I'm really glad that Zig is taking inspiration from Fil-C. However, I think Fil-C has a notable anti-Zig-like pattern, regarding how it defaults to zero-initialisation for everything.

To quote https://fil-c.org/gimso:

New capabilities have their payload initialized to zero.

The payload and shadow storage are zero-initialized. There is no such thing as uninitialized memory in Fil-C.

My understanding is that Fil-C deviates from usual C semantics by treating "uninitialised" reads to be safe behaviour, because they are technically initialised to zero.

I hope Zig doesn't (blindly) copy this approach, as I find zero-init to be bug prone in that zero is frequently (but not always) a meaningful state, and it's not a no-op, such that a program that relies on or assumes zero-init will have issues on optimised modes where the zero-init would be optimised out. This can break the assumption that optimisation should not introduce bugs, provided there is no illegal behaviour.

If the goal is to possibly depend on Fil-C when compiling C/C++ builds, maybe some extra data needs to track when memory is marked uninitialised or not.

I'm really glad that Zig is taking inspiration from Fil-C. However, I think Fil-C has a notable anti-Zig-like pattern, regarding how it defaults to zero-initialisation for everything. To quote https://fil-c.org/gimso: > New capabilities have their payload initialized to zero. > The payload and shadow storage are zero-initialized. There is no such thing as uninitialized memory in Fil-C. My understanding is that Fil-C deviates from usual C semantics by treating "uninitialised" reads to be safe behaviour, because they are technically initialised to zero. I hope Zig doesn't (blindly) copy this approach, as I find zero-init to be bug prone in that zero is _frequently_ (but not always) a meaningful state, and it's not a no-op, such that a program that relies on or assumes zero-init will have issues on optimised modes where the zero-init would be optimised out. This can break the assumption that optimisation should not introduce bugs, provided there is no illegal behaviour. If the goal is to possibly depend on Fil-C when compiling C/C++ builds, maybe some extra data needs to track when memory is marked uninitialised or not.

@dynamic_rendering wrote in :

Id like a clarification on how this is safer than Rust?

This would offer no escape hatches. (E.g., no unsafe keyword.)

@dynamic_rendering wrote in https://codeberg.org/ziglang/zig/issues/36237#issuecomment-19720379: > Id like a clarification on how this is safer than Rust? This would offer no escape hatches. (E.g., no `unsafe` keyword.)

So, if I'm understanding this correctly: instead of performing a singular, one time compile check, we are offloading the entire issue to runtime...?

So, if I'm understanding this correctly: instead of performing a singular, **one time compile check**, we are offloading the entire issue to **runtime...?**

@Barry1375 wrote in :

Funny how memory safety suddenly became a priority right after Bun moved to Rust. That feels like a pretty big departure from Zig's original philosophy.

Someone's still pretty butthurt about Jared and the Bun team.

You must be quite detached from the project and looking at it from the outside you might think like this, but Andrew is interested in Fil-C way before than this "bun rewrite" thing, sharing links in the IRC channel months ago.

@Barry1375 wrote in https://codeberg.org/ziglang/zig/issues/36237#issuecomment-19720925: > Funny how memory safety suddenly became a priority right after Bun moved to Rust. That feels like a pretty big departure from Zig's original philosophy. > > Someone's still pretty butthurt about Jared and the Bun team. You must be quite detached from the project and looking at it from the outside you might think like this, but Andrew is interested in Fil-C way before than this "bun rewrite" thing, sharing links in the IRC channel months ago.
ashwinin commented 2026-07-20 18:18:45 +02:00

This would offer no escape hatches. (E.g., no unsafe keyword.)

So an all-or-nothing scheme?

> This would offer no escape hatches. (E.g., no `unsafe` keyword.) So an all-or-nothing scheme?

@yrds wrote in :

@Barry1375 wrote in (comment):

Funny how memory safety suddenly became a priority right after Bun moved to Rust. That feels like a pretty big departure from Zig's original philosophy.
Someone's still pretty butthurt about Jared and the Bun team.

You must be quite detached from the project and looking at it from the outside you might think like this, but Andrew is interested in Fil-C way before than this "bun rewrite" thing, sharing links in the IRC channel months ago.

well, the Bun rewrite could move this idea up on his TODO list and provide fuel for Andrew's flamy tongue. I totally get the emotions, but please, someone, prohibit flaming and "Holy Wars" under Code of Conduct. the proposal is... curious, if a little contrary to Zig's stated philosophy, but it could totally be written without snark that obscures the technical beauty.

@yrds wrote in https://codeberg.org/ziglang/zig/issues/36237#issuecomment-19723160: > @Barry1375 wrote in #36237 (comment): > > > Funny how memory safety suddenly became a priority right after Bun moved to Rust. That feels like a pretty big departure from Zig's original philosophy. > > Someone's still pretty butthurt about Jared and the Bun team. > > You must be quite detached from the project and looking at it from the outside you might think like this, but Andrew is interested in Fil-C way before than this "bun rewrite" thing, sharing links in the IRC channel months ago. well, the Bun rewrite could move this idea up on his TODO list and provide fuel for Andrew's flamy tongue. I totally get the emotions, but please, someone, prohibit flaming and "Holy Wars" under Code of Conduct. the proposal is... curious, if a little contrary to Zig's stated philosophy, but it could totally be written without snark that obscures the technical beauty.

For reasons stated by others, this feels at-odds with Zig idiom and more an unnecessary "box-checking" to perhaps technically "win one over" on Rust, which in my view should not be a motivation for expansion.

For reasons stated by others, this feels at-odds with Zig idiom and more an unnecessary "box-checking" to perhaps technically "win one over" on Rust, which in my view should not be a motivation for expansion.

@ashwinin wrote in :

This would offer no escape hatches. (E.g., no unsafe keyword.)

So an all-or-nothing scheme?

The point is to not misbehave when a memory safety error occurs—that is, you end up with no exploitable memory safety errors. Rust still has these because of the unsafe keyword. Basically the difference is compile-time memory safety (what Rust offers if you don't use any unsafe blocks) and run-time memory safety (what Fil-C offers).

@ashwinin wrote in https://codeberg.org/ziglang/zig/issues/36237#issuecomment-19723844: > > This would offer no escape hatches. (E.g., no `unsafe` keyword.) > > So an all-or-nothing scheme? The point is to not <em>misbehave</em> when a memory safety error occurs—that is, you end up with no exploitable memory safety errors. Rust still has these because of the `unsafe` keyword. Basically the difference is compile-time memory safety (what Rust offers if you don't use any `unsafe` blocks) and run-time memory safety (what Fil-C offers).
Contributor

I suppose this ABI's main purpose is to be used for fuzzing (and generally for testing), to ensure that some inspected program has no Illegal Behaviour - akin to Rust's MIRI (though perhaps with better FFI support).

From my understanding Fil-C does not completely adhere to C's notions of UB: some behaviours which are considered Undefined in the spec are allowed by Fil-C (e.g. signed integer overflow, access of uninitialised memory, some memory reinterpretations). Other behaviours, which are fine according to the C spec seem to be disallowed in Fil-C (e.g. in some cases, having more than 16 (but less than 128) arguments in a function call). My question then, is whether this ABI is intended to check for every occurrence of IB, and never report false positives; or, like Fil-C, declare that some Illegal Behaviours will manifest as some safe operation at runtime?

I suppose this ABI's main purpose is to be used for fuzzing (and generally for testing), to ensure that some inspected program has no Illegal Behaviour - akin to [Rust's MIRI](https://github.com/rust-lang/miri/) (though perhaps with better FFI support). From my understanding Fil-C does not completely adhere to C's notions of UB: some behaviours which are considered Undefined in the spec are allowed by Fil-C (e.g. signed integer overflow, access of uninitialised memory, some memory reinterpretations). Other behaviours, which are fine according to the C spec seem to be disallowed in Fil-C (e.g. in some cases, [having more than 16](https://fil-c.org/calling_convention) (but less than 128) arguments in a function call). My question then, is whether this ABI is intended to check for every occurrence of IB, and never report false positives; or, like Fil-C, declare that some Illegal Behaviours will manifest as some safe operation at runtime?

This is a very big change. I think the main things to consider are the stability of the ABI and if is compatible with the Zig philosophy.

Besides that, I'm excited to see how this goes.

This is a very big change. I think the main things to consider are the stability of the ABI and if is compatible with the Zig philosophy. Besides that, I'm excited to see how this goes.
Sign in to join this conversation.
No milestone
No assignees
14 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
ziglang/zig#36237
No description provided.