danos/docs/os-development
Daniel Samson 36a7cc5fe9
kernel: ring 0 cannot execute a user page
SMEP turns the classic escalation — divert kernel control flow into a page
the attacker wrote — from a silent takeover into an immediate fault with
the offending address in the log. The bit is per-core state, so it is set
where the syscall MSRs already are: in the per-CPU bring-up both the boot
processor and every application processor run on their way in. A core that
climbed the trampoline without it would be a hole no boot log would show,
which is why the SMP case now reads CR4 on each core it lands on and
requires every one of them to be hardened, not just the one that printed
the banner.

Enabling it that early is only safe because nothing ring 0 executes is
mapped for ring 3, and that had to be established rather than assumed:
kernel text carries only its ELF flags, the physmap is no-execute, the
trampoline page is mapped supervisor and the core running it has not
enabled the bit yet, and the boot processor turns it on while still on the
loader's tables — which map nothing user-accessible at all. The one
indirect call in the kernel takes a kernel address.

The CPUID probing that was scattered across the timer code becomes a small
shared helper, since the feature question is now asked from two places and
each wanted the same maximum-leaf guard. Absence is tolerated and reported,
like the IOMMU: danos still boots on a machine without the feature, and
says which one it is.

The test harness starts asking QEMU for a CPU that has the bit at all —
its default model has neither SMEP nor SMAP, so the code would otherwise
have been unreachable in every run. No case behaved differently under the
richer model.

Suite 112/112, with a new case that maps an executable user page, calls
into it from the kernel, and requires the fault the CPU is supposed to
raise.
2026-08-01 10:05:58 +01:00
..
README.MD update docs/os-development/README.MD 2026-07-23 01:41:47 +01:00
acpi.md re-org docs 2026-07-23 00:25:34 +01:00
architecture.md re-org docs 2026-07-23 00:25:34 +01:00
arm.md re-org docs 2026-07-23 00:25:34 +01:00
communication.md docs: the communication stack — /protocol namespace, layered model, non-unix hierarchy, SMEP/SMAP plan 2026-07-31 18:11:28 +01:00
discovery.md init: /protocol replaces the ServiceId registry 2026-08-01 02:39:07 +01:00
efi.md docs: catch the docs up with the finished package split 2026-07-30 04:24:34 +01:00
frame-allocator.md re-org docs 2026-07-23 00:25:34 +01:00
framebuffer.md re-org docs 2026-07-23 00:25:34 +01:00
gop.md re-org docs 2026-07-23 00:25:34 +01:00
halting.md re-org docs 2026-07-23 00:25:34 +01:00
heap.md re-org docs 2026-07-23 00:25:34 +01:00
interrupts.md re-org docs 2026-07-23 00:25:34 +01:00
logging.md re-org docs 2026-07-23 00:25:34 +01:00
memory-map.md re-org docs 2026-07-23 00:25:34 +01:00
paging.md re-org docs 2026-07-23 00:25:34 +01:00
power.md library: the harness keeps the subscribers, and an id belongs to whoever opened it 2026-08-01 09:05:26 +01:00
process-lifecycle.md library: the harness keeps the subscribers, and an id belongs to whoever opened it 2026-08-01 09:05:26 +01:00
process-management.md re-org docs 2026-07-23 00:25:34 +01:00
protocol-namespace.md init: a protocol you were not granted does not exist 2026-08-01 04:44:45 +01:00
release-iso.md re-org docs 2026-07-23 00:25:34 +01:00
resilience.md re-org docs 2026-07-23 00:25:34 +01:00
scheduling.md re-org docs 2026-07-23 00:25:34 +01:00
shared-fate-plan.md re-org docs 2026-07-23 00:25:34 +01:00
smep-smap.md kernel: ring 0 cannot execute a user page 2026-08-01 10:05:58 +01:00
smp.md re-org docs 2026-07-23 00:25:34 +01:00
syscall.md re-org docs 2026-07-23 00:25:34 +01:00
system-image.md docs: the communication stack — /protocol namespace, layered model, non-unix hierarchy, SMEP/SMAP plan 2026-07-31 18:11:28 +01:00
sysv.md re-org docs 2026-07-23 00:25:34 +01:00
threading-plan.md init: /protocol replaces the ServiceId registry 2026-08-01 02:39:07 +01:00
threading.md init: /protocol replaces the ServiceId registry 2026-08-01 02:39:07 +01:00
timers.md re-org docs 2026-07-23 00:25:34 +01:00
vdso.md init: /protocol replaces the ServiceId registry 2026-08-01 02:39:07 +01:00

README.MD

OS Development

This document explains the architectural decisions behind the operating system.

Written in Zig?

The OS is written in Zig because it has excellent EFI support, so the OS boots quickly without a third-party bootloader.

Zig comes batteries included for systems work — cross-compilation, a build system, and a test runner are all part of the toolchain. Building with -Doptimize=ReleaseSafe keeps runtime safety checks on in the shipped kernel, which removes entire classes of bugs. The built-in test suite, combined with a QEMU integration harness, means every feature is proven to work, before it is shipped.

The codebase of the OS prioritizes readability. The aim is a codebase where someone new to OS development can find their way around without a guide.

A microkernel?

The kernel is a thin layer: it schedules processes and manages memory. Everything else — drivers, file systems, the display — runs in user space as separate, isolated processes.

The payoff is resilience. When a driver crashes, it doesn't take the OS down with it; it gets restarted. That makes this an ideal environment for developing an operating system, because a buggy driver is an ordinary bug: patch it, restart the service, and keep going.

There is a security benefit too. Processes are isolated and talk over Inter-Process Communication (IPC) channels, so compromising one service doesn't hand an attacker the whole machine. Vulnerabilities tend to stay contained in the process they started in.

Other operating systems choose to pack all of these duties into one binary as a Monolithic kernel, mostly for performance: a function call inside the kernel is faster than passing a message between isolated processes. That cost is real — an IPC round-trip is a few microseconds where a function call is nanoseconds — but it is also workload-shaped. Compute-bound programs don't notice it at all. For bulk data like file contents and pixels, the design moves data through shared memory and DMA so it is copied once, the same as a monolithic kernel; only small control messages cross the IPC boundary. What remains is the per-message cost on chatty paths, and the scheduler and memory management are designed to keep that small.

Private ABI

The syscall layer is private. The numbers and structures in abi.zig are an internal detail shared between the kernel and the system's own libraries, and they are free to change between builds.

The public boundary sits one level up: the vDSO that programs call into, and the documented IPC protocols such as the VFS protocol. Programs that stick to those interfaces keep working while the kernel rearranges itself underneath. This is the opposite of the Linux approach, where raw syscall numbers are frozen forever; here, stability is promised at the library and protocol level, and nowhere below it.

Steal the best bits and dump the legacy

The OS is Unix-like, but selectively. It borrows the ideas that have aged well — everything is a file, small services composed over clean interfaces — and skips the parts of POSIX that have caused decades of headaches.

Some concrete choices:

  • spawn, not fork. Creating a process starts a fresh program and returns the child's id. There is no clone-the-whole-address-space-then-immediately-throw-it-away dance, and none of the subtle state-inheritance bugs that come with it.
  • Time is a syscall. The kernel owns the clock and timers directly. There is no time daemon to keep alive and no ambiguity about where the truth lives.
  • Lifecycle events arrive as messages. A supervisor learns that a child exited through an IPC message on an endpoint it already owns — delivered like any other message, not as an interrupt that can fire between any two instructions.

The test for keeping an idea is simple: does it still pull its weight, or is it only there because it was there in 1979?