Fe-C 🦀#
Gradual memory-safety hardening for unsafe Rust and mixed-language programs.
Fe-C is the iron–carbon alloy system. Pure iron is soft; a controlled amount
of carbon, correctly alloyed, gives cast iron and steel. Safe Rust plus a
controlled amount of C-heritage unsafe, correctly checked, is stronger than
either. The name is also a sideways nod to Fil-C — read
Fe-C is not Fil-C before assuming anything about
guarantees.
What it does#
Safe Rust is proven at compile time, so Fe-C spends its runtime budget only where the type system abdicates:
- Deterministic boundary checks where raw pointers become safe pointers
(
&*p,&mut *p,Box::from_raw): bounds, liveness, and provenance are validated against a global allocation table at the cast site — failing at the root cause, not at a distant dereference. - FFI enforcement: libc symbol interposition (allocator family,
mem*, ptr+len syscall wrappers),extern "C"prologue checks, and sampled guard-page/canary allocations (GWP-ASan style) so even opaque C's own heap misuse traps in hardware. - A per-crate hardening dial, set like an optimization level.
Hardening modes#
| Mode | Metallurgy | Checked | Assumed |
|---|---|---|---|
--harden=case |
Case hardening: hard surface, fast ductile core | Raw→safe casts, FFI boundaries, alloc/free, optional re-checks after potential deallocation | Rust aliasing + validity rules hold inside elided (type-proven) regions; the optimizer shares those assumptions |
--harden=through |
Through hardening: uniform to the core | Every access, via opaque runtime calls the optimizer cannot reason around | Only the cementite runtime and syscall stubs (the same shape of trusted base Fil-C's runtime keeps) |
fil is a reserved alias for through, to be enabled only once the
guarantee is actually earned (precedent: Zig's proposed fil ABI mode).
Modes are per-crate, recorded in crate metadata, mediated by cross-mode call
adapters. Both modes are first-class from v0; see PLAN.md.
Fe-C is not Fil-C#
- Fil-C is unconditional: garbage in, memory safety out, for arbitrary code; its compiler never optimizes on assumptions the runtime doesn't enforce; the cost is ~1.5–4× everywhere.
- Fe-C
caseis conditional: deterministic detection of the bug classes Rust programs actually ship (unsafe/FFI misuse), at near-zero cost for safe code — but an aliasing-UB path can corrupt state without crossing a checked boundary. It is a testing/hardening tool, not a containment boundary for hostile code. - Fe-C
throughaims at the Fil-C-grade guarantee inside Rust's world, staged deliberately (see PLAN.md §5–7). - Fe-C does not check Rust's aliasing model in any mode — that is Miri's job; run both.
- For lifetime-bound bugs (too-relaxed signatures letting borrows outlive their
frame), Fe-C detects and contains the consequence; it does not prevent
the mistake. Correct signatures,
cargo audit, and static analysis do. casemode's free-during-scope detection is single-thread-sound: a concurrent free between re-check and dereference can be missed.throughmode has no such window.throughmode guarantees memory safety for programs free of data races on pointer slots (already UB in Rust). Fil-C is stronger here: it makes races harmless to memory unconditionally. Seedocs/through-mode-coherence.md.- If you need guaranteed containment today: Fil-C, CHERI hardware, or a wasm boundary.
Architecture#
One Rust workspace. No compiler fork, no LLVM linkage, no submodules.
| Crate | Role |
|---|---|
cargo-fe-c |
Cargo subcommand + RUSTC_WRAPPER; instruments the whole graph incl. std via -Zbuild-std |
fe-c-driver |
rustc-as-a-library (rustc_public where possible); MIR analysis + rewriting of accesses into plain runtime calls; per-crate mode metadata |
cementite |
The hard phase (Fe₃C): allocation table (never-recycled IDs + liveness epochs), check functions, quarantining #[global_allocator], libc interceptors |
Toolchain coupling budget: one pinned nightly in rust-toolchain.toml, bumped
by an automated pipeline (the Kani maintenance model).
Substrate#
Fe-C pairs with ../libc (vendored Eyra lineage) so that
"whole-process" means whole process: the libc's own unsafe is instrumented
like everyone else's. The unchecked residue shrinks to syscall stubs and a few
lines of asm.
Nix ❄️#
Everything routes through the flake:
nix build .#cargo-fe-c/.#cementitenix build .#fe-c-sysroot-case-x86_64— instrumentedcore/alloc/stdas cached derivations, rebuilt only on nightly bumpsnix develop— pinned nightly +rustc-dev+rust-src+ mirinix flake check— the CI entrypoint (fmt, lints, unit, RustSec corpus, false-positive suite, selfhost, miri-on-runtime); run by the tangled spindle pipeline
Status#
Design phase. Nothing here is a security claim yet.
- Starting work (human or agent): CLAUDE.md — hard rules, settled decisions, ordered task queue.
- Design record: PLAN.md. Evidence: docs/traces/.
Monorepo table row:
| [Fe-C 🦀](https://tangled.org/@overby.me/overby.me/tree/main/rust/fe-c) | Gradual memory-safety hardening for unsafe Rust and mixed-language programs |
Prior art#
Fil-C (InvisiCaps, FUGC) · SafeFFI (USENIX Security '26) · Miri · rustc_public · Kani (maintenance model) · CHERI rustc / strict provenance · Eyra · GWP-ASan · Zig #36237