Operating systems are usually associated with C.

That association is understandable. C is small, predictable, close to the machine, and historically dominant in kernel work. It gives the programmer direct access to memory, registers, layout, and calling conventions.

Those are exactly the things a kernel needs.

They are also exactly the things that make kernels hard to secure.

EriX is written primarily in Rust because the project is built around one central idea:

Authority should be explicit.

That applies to capabilities, boot handoff structures, IPC messages, memory objects, and device access. It also applies to implementation language. Rust does not make kernel development automatically safe, but it gives the codebase a way to make ownership, mutation, lifetimes, and unsafety visible.

That visibility matters.


The Kernel Is an Uncomfortable Place for Memory Bugs

Memory bugs are bad everywhere.

In a kernel, they are worse.

A use-after-free in an ordinary application can corrupt that application. A use-after-free in the kernel can corrupt scheduler state, capability tables, address spaces, or device mappings.

A bounds error in a user-space parser may crash one process. A bounds error in a boot parser may affect what code the machine executes next.

A bad pointer in an application is a local bug. A bad pointer in a kernel can be an authority bug.

This is why memory safety is not a cosmetic concern for EriX. The kernel is part of the trusted computing base. The bootloader is part of the trusted computing base. Several parser and ABI libraries are TCB or TCB-adjacent.

If those components mis-handle memory, the isolation model can fail before any capability check has a chance to help.


What Rust Gives a Kernel

Rust gives a kernel several useful defaults.

It gives bounds-checked slices instead of unchecked pointer-and-length pairs in ordinary code.

It gives ownership rules that make accidental aliasing harder.

It gives lifetimes that can express borrowed views into boot images, ELF binaries, handoff blobs, and IPC buffers without pretending those views own the underlying storage.

It gives Result and enums as ordinary tools for making error paths explicit.

It gives a type system strong enough to distinguish many states that C code would often represent as comments, flags, or conventions.

None of this removes the need for kernel design.

Rust will not decide what a capability means. It will not decide which service should receive a device endpoint. It will not prove that an IPC protocol is correct. It will not prevent every race in a scheduler.

But it changes the default.

In C, the default is that almost every function can do unchecked pointer arithmetic, alias mutable memory, reinterpret bytes as structures, and silently overflow unless the programmer is disciplined everywhere.

In Rust, ordinary code cannot do those things without passing through unsafe or an explicit checked operation.

That is a practical security difference.


no_std Is the Normal Environment

Kernel Rust is not ordinary application Rust.

The EriX kernel crate is #![no_std]. It does not run on top of an operating system. It is the operating system substrate.

Many EriX libraries are also no_std, including:

  • lib-bootimg
  • lib-elf
  • lib-handoff
  • lib-ipc
  • lib-capabi
  • lib-bootstrap
  • lib-console
  • lib-device
  • lib-driver
  • lib-time

Several of these crates also use #![deny(unsafe_code)].

That combination is important. It means the same libraries can be used in boot, kernel, and freestanding service contexts without depending on host OS services. It also means parser and ABI code can often be written without raw pointers at all.

This is one of Rust’s strongest fits for EriX.

The project has many small crates whose job is not to touch hardware directly. Their job is to parse, validate, encode, decode, and reject. Those are exactly the jobs where safe Rust is valuable.


Safe Parsers Are Security Boundaries

EriX treats parsers as security boundaries.

lib-bootimg parses and verifies boot.img. Its read path is part of the boot trust chain. It validates structure, alignment, bounds, hashes, and signatures before the bootloader trusts the image.

lib-elf parses ELF64 binaries before the bootloader uses load segments.

lib-handoff validates versioned handoff structures between boot stages and between the kernel and rootd.

lib-capabi defines capability types, rights, slot constants, transfer descriptors, and startup validation helpers.

These crates are deliberately boring.

They use byte slices. They use checked arithmetic. They return structured errors. They reject unsupported versions, bad sizes, bad offsets, and malformed tables.

They do not do I/O. They do not own runtime authority. They do not guess.

That design would be possible in C, but it would require sustained discipline:

  • every pointer must be checked
  • every offset must be range-checked
  • every integer operation must be audited for overflow
  • every returned view must be tied manually to the lifetime of the input buffer

Rust makes those rules cheaper to maintain.

For example, a parsed boot image can borrow from the original byte slice. A validated ELF load segment can expose borrowed bytes instead of manufacturing a new unchecked pointer. A handoff parser can return an iterator that checks each entry range before parsing it.

The language does not prove the parser is semantically perfect.

It does remove a large class of accidental memory mistakes from the parser’s normal code path.


unsafe Does Not Disappear

Kernel development cannot be entirely safe Rust.

The hardware interface is not type-safe. CPU registers, page tables, interrupt descriptors, port I/O, MMIO, DMA buffers, and boot entry pointers do not arrive as friendly Rust values.

At some point the kernel must say:

I know what this raw address means.

That statement is unsafe.

EriX has unsafe in the places where the machine forces it:

  • boot entry code converts a raw handoff pointer and length into a byte slice
  • x86_64 syscall glue uses inline assembly and a fixed register ABI
  • context switching uses architecture-specific assembly
  • GDT, IDT, TSS, and syscall initialization touch CPU descriptor state
  • page-table manipulation uses volatile reads and writes
  • serial and legacy device paths use port I/O
  • MMIO and DMA-facing drivers use volatile accessors
  • a few single-threaded runtimes use UnsafeCell behind documented static buffers

That list is not a failure of Rust.

It is the honest boundary between the language and the machine.

The goal is not to have no unsafe anywhere. The goal is to keep unsafe small, local, documented, and surrounded by safe interfaces.


Unsafe Boundaries Should Be Narrow

The important question is not:

Does the codebase contain unsafe?

The important question is:

Where is the unsafe, and what invariant makes it valid?

In EriX, the raw syscall bridge in ipc-syscall_x86_64 is a good example.

The crate exposes safe wrapper functions such as ipc_call, ipc_recv, ipc_reply, query_local_cap, and drop_local_cap. Internally, it has one raw assembly boundary that maps arguments to the x86_64 syscall register ABI and returns raw register values.

The wrapper does not dereference user pointers itself. It passes pointer and length values to the kernel, where capability checks, endpoint checks, and message-length validation happen.

That is the right shape:

  • raw assembly is isolated
  • the ABI is documented
  • the public API is typed
  • semantic validation remains in the kernel

The kernel’s context switch boundary follows the same pattern. There is architecture-specific assembly for saving and restoring registers, but the rest of the scheduler can work with a KernelContext structure and a ContextSwitcher trait. Host tests can use a soft switcher that models state handoff without actually jumping the CPU.

That split matters because it keeps most scheduler logic testable without executing privileged instructions.


Hardware Access Is Still Hardware Access

Rust does not make MMIO safe by itself.

If a driver writes the wrong value to the wrong register, Rust will not save the device. If a DMA buffer is described incorrectly, the hardware can still write to memory. If a page table entry is wrong, the CPU will enforce the wrong mapping very efficiently.

EriX handles this in two ways.

First, hardware authority is made explicit. A driver should not receive broad machine authority. It should receive only the device frame, I/O range, interrupt line, or endpoint family it needs.

Second, the unsafe code that touches hardware is kept near the hardware boundary.

For example, drv-virtio-block has volatile MMIO and DMA helper functions. That is expected for a block driver. The important point is that this code runs in a user-space driver with a narrow device authority surface, not inside a large monolithic kernel with full machine authority.

Rust helps inside that driver, but EriX still relies on architecture:

  • explicit device capabilities
  • typed device memory such as CAP_TYPE_DEVICE_FRAME
  • narrow kernel-control endpoints
  • user-space driver isolation
  • fail-closed startup validation

The language and the OS design reinforce each other.

Neither replaces the other.


Rust Matches Capability Thinking

Rust’s ownership model is not the same thing as a capability system.

A Rust borrow is not an EriX capability. A Rust type is not kernel-enforced authority. The compiler cannot know whether a process should be allowed to map a frame or send to an endpoint.

Still, Rust and capabilities fit together well.

Both encourage explicitness.

In EriX, a component can act only when it holds the right capability with the right rights. In Rust, code can mutate data only when it has the right kind of access to that data.

That similarity is useful culturally and mechanically.

It encourages APIs where authority is passed as a value, not discovered through global state. It encourages functions to state what they need. It encourages startup code to validate the actual capability bundle it received instead of assuming a slot number means permission.

lib-capabi is an example of this style. It does not enforce authority at runtime, but it defines the shared language for authority: capability type IDs, rights flags, slot constants, transfer descriptors, role helpers, and startup validation.

Because it is no_std and deny(unsafe_code), this authority vocabulary can be used broadly without turning every consumer into a raw-memory consumer.


The Comparison With C

C gives a kernel maximum control with minimal abstraction cost.

That is its strength.

It is also its weakness.

In C, a capability table can be indexed out of bounds. A message buffer can be copied with the wrong length. A packed on-disk structure can be read through an unaligned pointer. A pointer can outlive the storage it describes. An integer overflow can wrap into a smaller allocation. A function can accidentally mutate state through an alias the caller did not expect.

All of these can be avoided in C.

They are avoided by review, convention, static analysis, sanitizers, fuzzing, and discipline.

Those tools matter. EriX would still need review, tests, and fuzzing if it were written entirely in Rust.

The difference is that Rust moves many of those checks into the default language model.

When EriX parses a boot image, walks a transfer table, builds an IPC message, or validates a handoff envelope, the ordinary representation is a slice, enum, iterator, checked integer operation, or structured Result.

In C, the ordinary representation would often be a pointer, a length, a cast, and a promise.

Promises are not worthless.

But kernels accumulate promises quickly.

Rust reduces how many of those promises need to be trusted.


Rust Does Not Remove Design Bugs

Rust is not a security proof.

It does not prevent the wrong capability from being granted.

It does not prevent an endpoint from having overly broad rights.

It does not decide whether rootd, procd, or deviced should own a policy decision.

It does not automatically make a parser reject every semantically invalid file.

It does not remove side channels.

It does not make DMA coherent.

It does not make interrupt ordering simple.

It does not make scheduling fair.

This is why EriX still emphasizes architecture:

  • small TCB
  • explicit capabilities
  • narrow endpoints
  • deterministic startup
  • fail-closed behavior
  • clean-room dependencies
  • user-space services
  • role-specific driver authority

Rust improves the implementation substrate. It does not replace the security model.


Panic Is Not an Error Strategy

Kernel code needs a clear failure model.

In EriX, recoverable failures from external input should be returned as errors. Malformed handoff data, unsupported versions, invalid transfer descriptors, bad endpoint kinds, and missing authority should not be repaired silently.

They should fail explicitly.

Panic is reserved for internal invariant violations, not ordinary input validation.

This matters because Rust makes panic easy, but a kernel cannot treat panic as a normal control-flow mechanism. A parser in the TCB should not panic on malformed input. A boot path should not accidentally unwind through firmware state. A runtime service should not treat attacker-controlled input as a reason to enter an undefined recovery path.

The useful Rust pattern is:

  • validate before trusting
  • return Result
  • keep error variants explicit
  • reserve panic for impossible internal states
  • fail closed when security state is ambiguous

That pattern appears throughout the EriX parser and handoff crates.


Testing Benefits

Rust also helps testing.

The EriX kernel is a no_std crate, but much of its logic can still be tested on the host with #[cfg(test)] support.

This is useful for code that should not require a VM boot just to validate a pure invariant:

  • parser bounds checks
  • handoff validation
  • capability descriptor parsing
  • rights-mask logic
  • slot-policy tables
  • scheduler state transitions
  • soft context-switch models

The architecture-specific parts still need integration tests. Inline assembly, page-table manipulation, interrupt delivery, and user-mode transitions have to be tested in the environment where they actually run.

But Rust makes it easier to keep pure logic pure.

That is valuable in a microkernel, because many components can be reduced to small deterministic state machines around explicit messages and capabilities.


Why This Matters for EriX

EriX is not using Rust because Rust is fashionable.

It is using Rust because the project’s goals line up with the language’s strengths:

  • reduce memory-unsafety in trusted code
  • make ownership and mutation visible
  • isolate raw hardware access behind narrow boundaries
  • keep parser code safe, deterministic, and auditable
  • express protocol and authority data with typed structures
  • support no_std code shared across boot, kernel, and user-space services
  • make many bugs harder to write in the first place

The remaining unsafe code is still serious.

It must be reviewed as carefully as C would be reviewed. The difference is that EriX can concentrate that scrutiny on the places where the machine really does force raw control: entry code, CPU setup, page tables, syscalls, context switches, MMIO, DMA, and port I/O.

That is the practical value.

Rust does not make the kernel safe.

It makes the unsafe parts stand out.


Looking Ahead

The next step is to follow execution from the first firmware-loaded code into the kernel.

That path is where many of the ideas from the previous posts meet: the TCB, Rust boundaries, validated parsers, signed boot images, handoff structures, and the first transfer of machine authority into explicit kernel objects.

The next post will walk through the EriX boot process from firmware to kernel: the UEFI starting point, the bootloader’s handoff model, and why the kernel is entered as a higher-half executable.