Show HN: CeeSafe – Rust-style ownership for kernels, compiled to C11
How CeeSafe's approach to kernel safety differs from Rust's: the same ownership model, delivered as warning-clean C11 that the kernel's own build compiles.
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
How CeeSafe's approach to kernel safety differs from Rust's: the same ownership model, delivered as warning-clean C11 that the kernel's own build compiles.
Rust has shown that ownership and borrowing can remove whole classes of memory bugs from kernel code, and Rust in the Linux kernel is no longer an experiment. This post takes that result as given. The question it asks is narrower: what does a C kernel have to take on to get it?
With Rust, the answer includes a second compiler. Rust code is compiled by rustc through LLVM, beside the kernel's C compiler, which for most Linux builds is GCC, and the kernel's C headers reach Rust through bindings that bindgen generates with libclang. The kernel now has two toolchains to pin, two sets of target and code-generation options to keep in agreement, a copy of its struct layouts that a tool derives from the headers, and Rust code only on the architectures LLVM targets. A GCC front end for Rust and a GCC backend for rustc are being built to close that gap. That is a great deal of work spent on the toolchain question alone.
CeeSafe is a memory-safe systems language that makes a different trade. Its safety model is Rust's: single ownership, moves, shared and exclusive borrows, deterministic destruction, no garbage collector. Its compiler, ceesafec, does not produce machine code. It produces C11, and the kernel's existing C compiler, with the kernel's existing flags and headers, does the rest.
This post describes what that buys, what it costs, and what we have measured so far.
ceesafec build --profile freestanding --emit obj writes one relocatable object. From the kernel build's point of view, CeeSafe code is C files compiled with its own CFLAGS. The C backend's contract (spec/c-backend.md §1) says what those files are:
"Warning-clean" is not decoration. A kernel that builds with -Werror and a strict warning set will not accept generated code that is merely valid, and generated code that needs its own relaxed flags is a second toolchain in disguise.
The object's undefined symbols are a short, fixed list (spec/profiles.md §4.4): ceesafe_panic, memcpy, memmove, memset, memcmp, and the functions the package itself declares in extern "C" blocks. On the kernel targets the object has no constructor sections, no thread-local storage, no unwind tables, no COMMON symbols, and no mutable global state other than the program's own statics. A panic is a call to ceesafe_panic(kind, location), which the kernel defines; the CosmoOS binding passes it to the kernel's panic. The freestanding core runtime is a header of about 300 lines and a few C functions.
The compiler has the same property. ceesafec is written in CeeSafe, and every release carries a C seed, the compiler's own generated C, which one cc command turns into a working compiler on a platform that has no release build.
A kernel's structs change with its configuration. CONFIG_* options add and remove fields, and a debug build's spinlock_t is not a release build's.
CeeSafe never writes those layouts down. An extern "C" block names the header and the fields the CeeSafe code uses:
@header("kernel/timer.h") extern "C" { @ctype("struct timer") struct timer; @effects() fn timer_start(t: *mut timer, delay_ns: u64); }
The generated C #includes that header and accesses the real members, compiled with the build's own flags. The layout is therefore always the one of this configuration and architecture, because the same C compiler computed it from the same header. Each declared field is checked at C compile time: it exists and has exactly the declared type. A configuration that changes a field fails the build and names the field, where a stale binding would miscompile.
This is possible only because the output is C. A compiler that emits machine code must know the layout itself, so it needs a tool that reads the headers the way the C compiler would and a copy of the result.
Memory safety is not the only rule kernel code lives under. Code holding a spin lock must not sleep. An interrupt handler must not sleep or allocate. Ownership does not express either.
CeeSafe checks them as effects. Every function's effects, sleep and alloc, are inferred from what it does and calls, destructors included. @effects(…) declares what a C function may do, and @context(atomic) or @context(irq) says what a function, or the holder of a guard, must not do:
@header("kernel/sched.h") extern "C" { @effects(sleep) fn cosmo_sleep_ms(ms: u32); }
/// Holding a spin-lock guard makes the code that runs meanwhile atomic: it must not sleep. @context(atomic) struct SpinGuard { cpu: u32; drop {} }
/// Safety: cosmo_sleep_ms has no preconditions; it may sleep. fn backOff() { unsafe { cosmo_sleep_ms(1); } }
fn update(counter: borrow mut u64) { let guard = SpinGuard { cpu: 0 }; counter += 1; backOff(); // sleeps while the guard is held }
textmain.cee:17:5: error[N0295]: a 'SpinGuard' is live here, which makes this an atomic context (type-system.md §32.3), but this may sleep: it calls 'backOff', which calls 'cosmo_sleep_ms', declared @effects(sleep)
The error follows the call chain to the C function that sleeps. The same mechanism covers destructors: KRef<T> is a reference-counted kernel object whose final release may sleep, so dropping one under a spin lock does not compile.
Rust has no notion of "may sleep" in the language. For kernel Rust the same rule is checked by klint, a separate lint tool over annotated code. The difference is where the check lives: in CeeSafe it is part of the type checker that every build runs, with the effects inferred, and it is stated in the language specification.
The limit is stated in the guide and belongs here too: the check is only as good as the @effects written on the C declarations, and it says nothing about deadlocks or lock ordering. The kernel's lockdep remains the check for those.
The last point above is also the first cost.
The record is in docs/cosmoos-integrations.md, from runs on 2026-10-31 against an unchanged CosmoOS tree:
AArch64 boots were not part of that record.
On speed, with every check on, CeeSafe runs sixteen user-space workloads in 0.96× Rust's time (geometric mean) and 0.88× that of Rust built with overflow checks; hand-written C without checks takes 0.94×. CeeSafe is ahead where its compiler removes checks or allocations and behind by up to 1.41× on a hash map whose bounds checks it cannot yet remove. guide/performance.md has the table, the method and the losses. These are not kernel benchmarks.
Rust brings a kernel memory safety and asks it to adopt a compiler. CeeSafe brings the same ownership model, a weaker borrow checker and a much smaller ecosystem, and asks the kernel to compile some more C: C that passes its warning set, reads its headers, links against a five-symbol runtime, and maps back to source lines in its debugger. For a kernel whose build, targets and review process are organised around a C compiler, that is a different adoption cost, and it is the one CeeSafe was designed to minimise.
The Rust side of this comparison has been argued at length on Hacker News, and those threads are better sources for it than this post:
CeeSafe is GPL-2.0-only, with an exception for the runtime and library code compiled into programs. The guide's kernel chapter, the profiles specification and CeeSafe for Rust programmers go further than this post.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。