Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proposal: Go Language Support via Custom GOOS

Running Go programs natively on capOS by implementing a GOOS=capos target in the Go runtime.

Motivation

Go is the implementation language of CUE, the configuration language planned for system manifests. Beyond CUE, Go has a large ecosystem of systems software (container runtimes, network tools, observability agents) that would be valuable to run on capOS without rewriting.

The userspace-binaries proposal (Part 3) places Go in Tier 4 (“managed runtimes, much later”) and suggests WASI as the pragmatic path. This proposal explores the native alternative: a custom GOOS=capos that lets Go programs run directly on capOS hardware, without a WASM interpreter in between.

Why Go is Hard

Go’s runtime is a userspace operating system. It manages its own:

  • Goroutine scheduler — M:N threading (M OS threads, N goroutines), work-stealing, preemption via signals or cooperative yield points
  • Garbage collector — concurrent, tri-color mark-sweep, requires write barriers, stop-the-world pauses, and memory management syscalls
  • Stack management — segmented/copying stacks with guard pages, grow/shrink on demand
  • Network poller — epoll/kqueue-based async I/O for net.Conn
  • Memory allocator — mmap-based, spans, mcache/mcentral/mheap hierarchy
  • Signal handling — goroutine preemption, crash reporting, profiling

Each of these assumes a specific OS interface. The Go runtime calls ~40 distinct syscalls on Linux. capOS currently has 2.

Syscall Surface Required

The Go runtime’s Linux syscall usage, grouped by subsystem:

Memory Management (critical, blocks everything)

Go runtime needsLinux syscallcapOS equivalent
Heap allocationmmap(MAP_ANON)FrameAllocator cap + page table manipulation
Heap deallocationmunmapUnmap + free frames
Stack guard pagesmmap(PROT_NONE) + mprotectMap page with no permissions
GC needs contiguous arenasmmap with hintsAllocate contiguous frames, map contiguously
Commit/decommit pagesmadvise(DONTNEED)Unmap or zero pages

capOS needs: A sys_mmap-like capability or syscall that can:

  • Map anonymous pages at arbitrary user addresses
  • Set per-page permissions (R, W, X, none)
  • Allocate contiguous virtual ranges
  • Decommit without unmapping (for GC arena management)

This could be a VirtualMemory capability:

interface VirtualMemory {
    # Map anonymous pages at hint address (0 = kernel chooses)
    map @0 (hint :UInt64, size :UInt64, prot :UInt32) -> (addr :UInt64);
    # Unmap pages
    unmap @1 (addr :UInt64, size :UInt64) -> ();
    # Change permissions on mapped range
    protect @2 (addr :UInt64, size :UInt64, prot :UInt32) -> ();
    # Decommit (release physical frames, keep virtual range reserved)
    decommit @3 (addr :UInt64, size :UInt64) -> ();
}

Threading (critical for goroutines)

Go runtime needsLinux syscallcapOS equivalent
Create OS threadclone(CLONE_THREAD)Thread cap or in-process threading primitive
Thread-local storagearch_prctl(SET_FS)Per-thread FS base (kernel sets on context switch)
Block threadfutex(WAIT)Futex cap or kernel-side futex
Wake threadfutex(WAKE)Futex cap
Thread exitexit(thread)Thread exit syscall

capOS needs: Threading support within a process. Options:

Option A: Kernel threads. The kernel manages threads (multiple execution contexts sharing one address space). Each thread has its own stack, register state, and FS base, but shares page tables and cap table with the process. This is what Linux does and what Go expects.

Option B: User-level threading. The process manages its own threads (like green threads). The kernel only sees one execution context per process. Go’s scheduler already does M:N threading, so it could work with a single OS thread per process — but the GC’s stop-the-world relies on being able to stop other OS threads, and the network poller blocks an OS thread.

Option A is simpler for Go compatibility. Option B is more capability-aligned (threads are a process-internal concern) but requires Go runtime modifications.

Synchronization

Go runtime needsLinux syscallcapOS equivalent
Futex waitfutex(FUTEX_WAIT)Futex authority cap, ABI selected by measurement
Futex wakefutex(FUTEX_WAKE)Futex authority cap, ABI selected by measurement
Atomic compare-and-swapCPU instructionsAlready available (no kernel support needed)

Futexes are a kernel primitive (block/wake on a userspace address). capOS should expose futex authority through a capability from the start. The ABI is still a measurement question: generic capnp/ring method if overhead is close to a compact path, otherwise a compact capability-authorized operation.

Time

Go runtime needsLinux syscallcapOS equivalent
Monotonic clockclock_gettime(MONOTONIC)Timer cap .now()
Wall clockclock_gettime(REALTIME)Timer cap or RTC driver
Sleepnanosleep or futex with timeoutTimer cap .sleep() or futex timeout
Timer eventstimer_create / timerfdTimer cap with callback or poll

Timer cap already planned. Go needs monotonic time for goroutine scheduling and wall time for time.Now().

I/O

Go runtime needsLinux syscallcapOS equivalent
Network I/Oepoll_create, epoll_ctl, epoll_waitAsync cap invocation or poll cap
File I/Oread, write, open, closeNamespace + Store caps (via POSIX layer)
Stdout/stderrwrite(1, ...), write(2, ...)Console cap
Pipe (runtime internal)pipe2IPC caps or in-process channel

Go’s network poller (netpoll) is pluggable per-OS — each GOOS provides its own implementation. For capOS, it would use async capability invocations or a polling interface over socket caps.

Signals (for preemption)

Go runtime needsLinux syscallcapOS equivalent
Goroutine preemptiontgkill + SIGURGThread preemption mechanism
Crash handlingsigaction(SIGSEGV)Page fault notification
Profilingsigaction(SIGPROF) + setitimerProfiling cap (optional)

Go 1.14+ uses asynchronous preemption: the runtime sends SIGURG to a thread to interrupt a long-running goroutine. On capOS, alternatives:

  • Cooperative preemption only. Go inserts yield points at function prologues and loop back-edges. This works but means tight loops without function calls won’t yield. Acceptable for initial support.
  • Timer interrupt notification. The kernel notifies the process (via a cap invocation or a signal-like mechanism) when a time quantum expires. The notification handler in the Go runtime triggers goroutine preemption.

Implementation Strategy

Phase 1: Minimal GOOS (single-threaded, cooperative)

Fork the Go toolchain, add GOOS=capos GOARCH=amd64. Implement the minimum runtime changes:

What to implement:

  • osinit() — read Timer cap from CapSet for monotonic clock
  • sysAlloc/sysFree/sysReserve/sysMap — translate to VirtualMemory cap
  • newosproc() — stub (single OS thread, M:N scheduler still works with M=1)
  • futexsleep/futexwake — spin-based fallback (no real futex yet)
  • nanotime/walltime — Timer cap
  • write() (for runtime debug output) — Console cap
  • exit/exitThread — sys_exit
  • netpoll — stub returning “nothing ready” (no async I/O)

What to stub/disable:

  • Signals (no SIGURG preemption, cooperative only)
  • Multi-threaded GC (single-thread STW is fine initially)
  • CGo (no C interop)
  • Profiling
  • Core dumps

Deliverable: GOOS=capos go build ./cmd/hello produces an ELF that runs on capOS, prints “Hello, World!”, and exits.

Estimated effort: ~2000-3000 lines of Go runtime code (mostly in runtime/os_capos.go, runtime/sys_capos_amd64.s, runtime/mem_capos.go). Reference: runtime/os_js.go (WASM target) is ~400 lines; runtime/os_linux.go is ~700 lines. capOS sits between these.

Phase 2: Kernel Threading + Futex

Add kernel support for:

  • Multiple threads per process (shared address space, separate stacks)
  • Futex authority capability and measured wait/wake ABI
  • FS base per-thread (for goroutine-local storage)

Update Go runtime:

  • newosproc() creates a real kernel thread
  • futexsleep/futexwake use the selected futex capability ABI
  • GC runs concurrently across threads
  • Enable GOMAXPROCS > 1

Deliverable: Go programs use multiple CPU cores. GC is concurrent.

Phase 3: Network Poller

Implement runtime/netpoll_capos.go:

  • Register socket caps with the poller
  • Use an async notification mechanism (capability-based poll() or notification cap)
  • net.Dial(), net.Listen(), http.Get() work

This depends on the networking stack being available as capabilities.

Deliverable: Go HTTP client/server runs on capOS.

Phase 4: CUE on capOS

With Go working, CUE runs natively. This enables:

  • Runtime manifest evaluation (not just build-time)
  • Dynamic service reconfiguration via CUE expressions
  • CUE-based policy enforcement in the capability layer

Kernel Prerequisites

PrerequisiteRoadmap StageWhy
Capability syscallsStage 4 (sync path done)Go runtime invokes caps (VirtualMemory, Timer, Console)
SchedulingStage 5 (core done)Go needs timer interrupts for goroutine preemption fallback
IPC + cap transferStage 6Go programs are service processes that export/import caps
VirtualMemory capabilityStage 5mmap equivalent for Go’s memory allocator and GC
Thread supportExtends Stage 5Multiple execution contexts per process
Futex authority capabilityExtends Stage 5Go runtime synchronization

VirtualMemory Capability

This is the biggest new kernel primitive. Go’s allocator requires:

  1. Reserve large virtual ranges without committing physical memory (Go reserves 256 TB of virtual space on 64-bit systems)
  2. Commit pages within reserved ranges (back with physical frames)
  3. Decommit pages (release frames, keep virtual range reserved)
  4. Set permissions (RW for data, none for guard pages)

The existing page table code (kernel/src/mem/paging.rs) supports mapping and unmapping individual pages. It needs to be extended with:

  • Virtual range reservation (mark ranges as reserved in some bitmap/tree)
  • Lazy commit (map as PROT_NONE initially, page fault handler commits on demand — or explicit commit via cap call)
  • Permission changes on existing mappings

Thread Support

Extending the process model (kernel/src/process.rs). See the SMP proposal for the PerCpu struct layout (per-CPU kernel stack, saved registers, FS base); Thread extends this for multi-thread-per-process. See also the In-Process Threading section in ROADMAP.md for the roadmap-level view.

#![allow(unused)]
fn main() {
struct Process {
    pid: u64,
    address_space: AddressSpace,  // shared by all threads
    caps: CapTable,               // shared by all threads
    threads: Vec<Thread>,
}

struct Thread {
    tid: u64,
    state: ThreadState,
    kernel_stack: VirtAddr,
    saved_regs: RegisterState,    // rsp, rip, etc.
    fs_base: u64,                 // for thread-local storage
}
}

The scheduler (Stage 5) schedules threads, not processes. Each thread gets its own kernel stack and register save area. Context switch saves/restores thread state. Page table switch only happens when switching between threads of different processes.

Alternative: Go via WASI

For comparison, the WASI path from the userspace-binaries proposal:

Native GOOSWASI
PerformanceNative speed~2-5x overhead (wasm interpreter/JIT)
Go compatibilityFull (after Phase 3)Limited (WASI Go support is experimental)
GoroutinesReal M:N schedulingSingle-threaded (WASI has no threads yet)
Net I/ONative async via pollerBlocking only (WASI sockets are sync)
Kernel workVirtualMemory, threads, futexNone (wasm runtime handles it)
Go runtime forkYes (maintain a fork)No (upstream GOOS=wasip1)
GCFull concurrent GCConservative GC (wasm has no stack scanning)
Maintenance burdenHigh (track Go releases)Low (upstream supported)

WASI is easier but limited. Go on WASI (GOOS=wasip1) is officially supported but experimental — no goroutine parallelism, no async I/O, limited stdlib. For running CUE (which is CPU-bound evaluation, no I/O, single goroutine), WASI might be sufficient.

Native GOOS is harder but complete. Full Go with goroutines, concurrent GC, network I/O, and the entire stdlib. Required for Go network services or anything using net/http.

Recommendation: Start with WASI for CUE evaluation (Phase 4 of the WASI proposal in userspace-binaries). If Go network services become a goal, invest in the native GOOS.

Relationship to Other Proposals

  • Userspace binaries proposal — this extends Tier 4 (managed runtimes) with concrete Go implementation details. The POSIX layer (Part 4) is NOT sufficient for Go — Go doesn’t use libc on Linux, it makes raw syscalls. The GOOS approach bypasses POSIX entirely.
  • Service architecture proposal — Go services participate in the capability graph like any other process. The Go net poller (Phase 3) uses TcpSocket/UdpSocket caps from the network stack.
  • Storage and naming proposal — Go’s os.Open()/os.Read() map to Namespace + Store caps via the GOOS file I/O implementation. Go doesn’t use POSIX for this — it has its own runtime/os_capos.go with direct cap invocations.
  • SMP proposal — Go’s GOMAXPROCS uses multiple OS threads (Phase 2). Requires per-CPU scheduling from Stage 5/7.

Open Questions

  1. Fork maintenance. A GOOS=capos fork must track upstream Go releases. How much drift is acceptable? Could the capOS-specific code eventually be upstreamed (like Fuchsia’s was)?

  2. CGo support. Go’s FFI to C (cgo) requires a C toolchain and dynamic linking. Should capOS support cgo, or is pure Go sufficient? CUE doesn’t use cgo, but some Go libraries do.

  3. GOROOT on capOS. Go programs expect $GOROOT/lib at runtime for some stdlib features. Where does this live on capOS? In the Store? Baked into the binary via static compilation?

  4. Go module proxy. go get needs HTTP access. On capOS, this would use a Fetch cap. But cross-compilation on the host is more practical than building Go on capOS itself.

  5. Debugging. Go’s runtime/debug and pprof expect signals and /proc access. What debugging capabilities should capOS expose?

  6. GC tuning. Go’s GC is tuned for Linux’s mmap semantics (decommit is cheap, virtual space is nearly free). capOS’s VirtualMemory cap needs to match these assumptions or the GC will need retuning.

Estimated Scope

PhaseNew kernel codeGo runtime changesDependencies
Phase 1: Minimal GOOS~200 (VirtualMemory cap)~2000-3000Stages 4-5
Phase 2: Threading~500 (threads, futex)~500Stage 5, SMP
Phase 3: Net poller~100 (async notification)~300Networking, Stage 6
Phase 4: CUE on capOS00Phase 1 (or WASI)
Total~800~2800-3800

Plus ongoing maintenance to track Go upstream releases.