Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 43 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,48 @@ on:
pull_request:

jobs:
ci:
uses: tschk/ci-recipes/.github/workflows/inauguration-ci.yml@master
with:
inauguration-path: vendor/inauguration
install-deps: "nasm"
build-command: |
mkdir -p /tmp/space-boot
command nasm -f bin boot/multiboot.asm -o /tmp/space-boot/trampoline.bin
command in compile --path kernel/kernel-root.in --entry kernel-entry --emit boot \
--trampoline /tmp/space-boot/trampoline.bin \
--target native --target-triple x86_64-unknown-none --linkage static-lib \
--out /tmp/space-boot/kernel.bin
test-command: |
echo "Boot image: $(wc -c < /tmp/space-boot/kernel.bin) bytes"
bash scripts/check-spdp-protocol.sh
bash scripts/check-sci-contract.sh
bash scripts/check-architecture-boundaries.sh
kernel:
name: Kernel compile and QEMU smoke
runs-on: ubuntu-latest
timeout-minutes: 45
env:
INAUGURATION_DIR: ${{ github.workspace }}/vendor/inauguration
BUILD_DIR: /tmp/space-boot
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/checkout@v4
with:
repository: tschk/inauguration
ref: 2e3bf260c9624d5c6127c56febcd0e1938e8d475
path: vendor/inauguration
persist-credentials: false
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Build in compiler
run: cargo build --release --bin in --features extended
working-directory: vendor/inauguration/in-cli
- name: Add in to PATH
run: echo "${{ github.workspace }}/vendor/inauguration/in-cli/target/release" >> "$GITHUB_PATH"
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y nasm qemu-system-x86
- name: Build boot image
run: |
mkdir -p "$BUILD_DIR"
nasm -f bin boot/multiboot.asm -o "$BUILD_DIR/trampoline.bin"
command in compile --path kernel/kernel-root.in --entry kernel-entry --emit boot \
--trampoline "$BUILD_DIR/trampoline.bin" \
--target native --target-triple x86_64-unknown-none --linkage static-lib \
--out "$BUILD_DIR/kernel.bin"
- name: Host gates and QEMU smoke
run: |
echo "Boot image: $(wc -c < "$BUILD_DIR/kernel.bin") bytes"
bash scripts/check-spdp-protocol.sh
bash scripts/check-sci-contract.sh
bash scripts/check-architecture-boundaries.sh
KERNEL_BIN="$BUILD_DIR/kernel.bin" bash scripts/check-qemu-smoke.sh

website:
name: Website gates
Expand All @@ -36,7 +61,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: "1.3.14"
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ kernel/target/
build/
.pi-subagents/
serial.log
.wrangler/
mon.sock
s.in
s.out
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ Space is a component-based operating system built on a five-layer architecture:
```

The native model is **component + capability + object + execution graph** — not
process + file + syscall + user. There is no POSIX in the kernel. Linux, Darwin,
and Windows compatibility are `.in` microservices that translate legacy concepts
into Space primitives.
process + file + syscall + user. Linux, Darwin, and Windows personalities are
kernel-linked `.in` translators in the same boot image (`components/linux.in`,
`darwin.in`, `windows.in`, `posix.in`). They are not isolated processes.

Domains currently clone the 4 GiB identity map and run at CPL0. That is not
isolation. See [`architecture.md`](architecture.md).

## Status

Expand Down Expand Up @@ -64,14 +67,17 @@ Measured via serial output polling on Apple M3 (macOS, QEMU TCG).

## Build and run

Requirements: `clang`, `nasm`, `qemu-system-x86_64`, and Inauguration (git
submodule under `vendor/inauguration`, or a sibling checkout at `../inauguration`).
Requirements: `clang`, `nasm`, `qemu-system-x86_64`, and Inauguration. The
`vendor/inauguration` submodule is the compiler pin (CI uses the same git
ref). `INAUGURATION_DIR` overrides it; otherwise the pin, then a sibling
checkout at `../inauguration`.

```sh
git submodule update --init --recursive
```

```sh
bash scripts/check-all.sh # static gates + QEMU boot + audit hardening
bash scripts/check-qemu-boot.sh # full boot verification
bash scripts/build-multicomponent.sh # SCI component loading demo
bash scripts/check-sci-contract.sh # metadata validation
Expand Down
56 changes: 37 additions & 19 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ SCI is the native binary contract — not ELF. An SCI artifact contains:
- Import/export table
- Provenance (compiler version, source hash)

The loader validates declared capabilities against the realm's grants
before transferring control. A component requesting undeclared
capabilities is denied before its entry point runs.
The loader compares a component's self-declared capability bitmask to a
kernel grant constant (packed SCI) or `SCI-GUEST-GRANTS` (file SCI) and
denies a superset. That is not hardware isolation. Syscalls consult
`cap-check` against the current domain's minted cap index; mint/revoke
from userspace return `-1`.

---

Expand All @@ -100,7 +102,7 @@ x86_64. It provides:
- **Preemptive scheduler** — timer-driven context switching
- **Typed channels** — CSP-style ring buffers with blocking send/recv
- **Cross-domain channels** — shared-page IPC between memory domains
- **Memory domains** — isolated page table trees (Phase 0)
- **Memory domains** — per-component page tables that currently clone the 4 GiB identity map (not a security boundary)
- **SCI loader** — loads and validates external component images
- **e1000 NIC driver** — MMIO register access, TX/RX rings, ARP, UDP
- **Deterministic execution** — xorshift64 PRNG with seeded workloads
Expand All @@ -118,11 +120,24 @@ x86_64. It provides:

## Memory Domains

Domains are isolated page table trees. Domain 0 is the kernel domain
(shared PML4 at physical 0x1000). `domain_create()` allocates a new
PML4, copies the kernel's low mappings, and returns an ID.
`domain_switch()` changes CR3. `domain_map()` installs a mapping in
a domain's page table. Shared pages enable cross-domain IPC.
Domains are separate page-table trees, not a security boundary today.

- Domain 0 is the kernel domain. After boot the live kernel PML4 is relocated
off physical `0x1000` onto a heap frame.
- `domain_create()` allocates a new PML4 and **copies the kernel's four page
directories** — the trampoline's 4 GiB identity map (`P|W|PS`, no NX).
Extra SCI image/heap/shared mappings are additive. A guest can still
`load64(0x200000)` (kernel globals) and reach MMIO.
- All component entry is CPL0 (`CS=0x08`). The GDT has no DPL3 segments.
`cr3_write` is bound into a kernel global at `domain-init` and the
published pointer at `0x4060` is cleared; guests still run at ring 0, so
they can execute privileged instructions until CPL3+`iret` exists.
- `domain_switch()` changes CR3. `domain_map()` installs a mapping.
Shared pages are for IPC, not isolation.

Exclusive maps plus CPL3 are required before domains can be advertised as
isolation. Do not land exclusive maps without a CPL3 trampoline: `invoke1`
under guest CR3 still needs kernel text mapped.

---

Expand All @@ -141,7 +156,10 @@ the component image, and transfers control.
## Capabilities

Capability slots are 16 bytes: `[target_object_ptr][rights]`. The
kernel mints capabilities into a root table. Capability bits:
kernel mints capabilities into a root table. Each domain records a cap
index; `sys-write` / `sys-read` require `cap-serial`, channel syscalls
require `cap-graph`. `sys-cap-mint` / `sys-cap-revoke` always return `-1`.
`sys-cap-check` may only query the caller's current cap.

| Bit | Capability |
|-----|-----------|
Expand All @@ -151,7 +169,8 @@ kernel mints capabilities into a root table. Capability bits:
| 8 | graph |

The loader rule: a component may only activate if its declared
authority is a subset of what its realm grants.
authority is a subset of the grant mask for that load path. Bits are
metadata plus syscall gates; they do not stop CPL0 `inb`/`outb`.

---

Expand Down Expand Up @@ -187,12 +206,11 @@ Native Space syscalls (0-4): write, read, exit, yield, getpid.

## Linux Personality

The Linux personality (`kernel/linux.in`) translates Linux x86_64
syscall numbers into Space kernel primitives, providing a POSIX-compatible
interface on top of the native component model. This is the first OS
personality (Phase 5), demonstrating that Space can host foreign ABIs
by mapping their conventions onto the underlying capability/domain/channel
substrate.
The Linux personality (`components/linux.in`) is **kernel-linked**. It
translates Linux x86_64 syscall numbers into Space kernel primitives.
`linux-init` may `domain-switch` into a POSIX helper domain, but the
translator is the same boot image as the nanokernel, not an isolated
microservice.

Implemented POSIX syscalls:

Expand Down Expand Up @@ -275,8 +293,8 @@ designed in `docs/compositor-client-split.md`.
self-test, Linux-personality demo, VFS, time service, network traffic,
component deny policy, and external display/input SCI components.
- SCI metadata-sidecar validation passes.
- Display and input SCI components boot in isolated domains under an automated
QEMU check.
- Display and input SCI components boot in separate (still identity-mapped,
CPL0) domains under an automated QEMU check.

### Component Transition
- Storage, network, and POSIX source has moved into `components/`, with
Expand Down
38 changes: 33 additions & 5 deletions components/domain.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,37 @@ const DOMAIN-ACTIVE = 1
var domain-table: Int = 0
var domain-count: Int = 0
var domain-current: Int = 0
var current-cap: Int = -1
var cr3-read-stub: Int = 0
var cr3-write-stub: Int = 0

fn cr3-read() -> Int {
return invoke1(cr3-read-stub, 0)
}

fn cr3-write(pml4: Int) -> void {
invoke1(cr3-write-stub, pml4)
return
}

fn domain-set-cap(id: Int, cap-idx: Int) -> void {
if id < 0 || id >= domain-count {
return
}
store64(domain-table + id * 24 + 16, cap-idx)
if domain-current == id {
current-cap = cap-idx
}
return
}

fn domain-init() -> void {
domain-table = alloc(MAX-DOMAINS * 24)
let i = 0
while i < MAX-DOMAINS {
store64(domain-table + i * 24 + 0, 0)
store64(domain-table + i * 24 + 8, DOMAIN-FREE)
store64(domain-table + i * 24 + 16, 0)
store64(domain-table + i * 24 + 16, -1)
i = i + 1
}
// The bootloader left the kernel PML4 at 0x1000, but the compiler may place
Expand All @@ -29,11 +52,15 @@ fn domain-init() -> void {
store64(kernel-pml4 + k * 8, load64(0x1000 + k * 8))
k = k + 1
}
invoke1(load64(0x4060), kernel-pml4)
cr3-read-stub = load64(0x4058)
cr3-write-stub = load64(0x4060)
store64(0x4058, 0)
store64(0x4060, 0)
cr3-write(kernel-pml4)
store64(0x40B8, kernel-pml4)
store64(domain-table + 0, kernel-pml4)
store64(domain-table + 8, DOMAIN-ACTIVE)
store64(domain-table + 16, 0)
store64(domain-table + 16, -1)
domain-count = 1
domain-current = 0
return
Expand Down Expand Up @@ -74,7 +101,7 @@ fn domain-create() -> Int {
let pml4 = create-domain-pml4()
store64(domain-table + id * 24 + 0, pml4)
store64(domain-table + id * 24 + 8, DOMAIN-ACTIVE)
store64(domain-table + id * 24 + 16, 0)
store64(domain-table + id * 24 + 16, -1)
domain-count = domain-count + 1
return id
}
Expand All @@ -84,8 +111,9 @@ fn domain-switch(id: Int) -> void {
if load64(domain-table + id * 24 + 8) != DOMAIN-ACTIVE { return }
let pml4 = load64(domain-table + id * 24 + 0)
if pml4 == 0 { return }
invoke1(load64(0x4060), pml4)
cr3-write(pml4)
domain-current = id
current-cap = load64(domain-table + id * 24 + 16)
return
}

Expand Down
2 changes: 1 addition & 1 deletion components/linux.in
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fn linux-init() -> void {
}
posix-domain = domain-create()
if posix-domain <= 0 {
posix-domain = 1
return
}
let shared-phys = domain-create-shared-page(0, posix-domain, LINUX-POSIX-VIRT, LINUX-POSIX-VIRT)
if shared-phys == 0 {
Expand Down
4 changes: 2 additions & 2 deletions components/memory.in
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn discover-memory(mb: Int) -> void {
}

// --- virtual memory management ----------------------------------------------
// The boot trampoline identity-maps the first 1 GiB with 2 MiB pages. These
// The boot trampoline identity-maps the first 4 GiB with 2 MiB pages. These
// helpers allocate fresh 4 KiB-aligned frames and install new 4 KiB mappings by
// walking and growing the page tables, the foundation for realm memory domains.
var free-list: Int = 0 // head of LIFO free list, 0 = empty
Expand Down Expand Up @@ -161,7 +161,7 @@ fn pt-ensure-pte(pml4: Int, virt: Int) -> Int {
}

fn map-page(virt: Int, phys: Int, flags: Int) -> void {
let pml4 = invoke1(load64(0x4058), 0)
let pml4 = cr3-read()
let pte = pt-ensure-pte-flags(pml4, virt, flags)
store64(pte, phys | flags)
invlpg(virt)
Expand Down
2 changes: 1 addition & 1 deletion components/network.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This file contains the actual Intel 82540EM driver implementation and a
// component entry point that services packet send/receive requests from the
// kernel over cross-domain channels. The kernel-side stubs live in
// kernel/net.in.
// components/net.in.

var nic-mmio: Int = 0
var nic-tx-ring: Int = 0
Expand Down
4 changes: 2 additions & 2 deletions components/nvme.in
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ fn nvme-start-storage-component(port: Int) -> Int {
return 0
}
storage-domain = domain-create()
if storage-domain == 0 {
storage-domain = 1
if storage-domain <= 0 {
return -1
}

let shared-mailbox = domain-create-shared-page(0, storage-domain, STORAGE-MAILBOX-VIRT, STORAGE-MAILBOX-VIRT)
Expand Down
4 changes: 4 additions & 0 deletions components/object.in
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ fn cap-mint(target-obj: Int, rights: Int) -> Int {
return idx
}

fn cap-require(rights: Int) -> Int {
return cap-check(current-cap, rights)
}

fn cap-check(idx: Int, rights: Int) -> Int {
if idx < 0 || idx >= cap-count {
return 0
Expand Down
5 changes: 4 additions & 1 deletion components/preempt.in
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ fn schedule-tick(ctx: Int) -> Int {
if preempt-pml4 != 0 {
let pml4 = load64(preempt-pml4 + pcurrent * 8)
if pml4 != 0 {
invoke1(load64(0x4060), pml4)
cr3-write(pml4)
let d = load64(preempt-domain + pcurrent * 8)
domain-current = d
current-cap = load64(domain-table + d * 24 + 16)
}
}
return load64(ptasks + pcurrent * 8)
Expand Down
13 changes: 12 additions & 1 deletion components/process.in
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,13 @@ fn sci-load-file(port: Int, pathname: Int) -> Int {
serial-write-hex(port, image-size)
serial-nl(port)

let missing = required & (-1 ^ realm-grants)
if sci-image-acceptable(entry, image-size) == 0 {
serial-write-cstr(port, " SCI: invalid entry or image-size")
serial-nl(port)
return -1
}

let missing = required & (-1 ^ SCI-GUEST-GRANTS)
if missing != 0 {
serial-write-cstr(port, " SCI: DENIED undeclared cap 0x")
serial-write-hex(port, missing)
Expand Down Expand Up @@ -314,6 +320,11 @@ fn sci-load-file(port: Int, pathname: Int) -> Int {
sci-last-caps = required

let component = component-register(pathname, entry, required)
if sci-bind-domain-cap(domain, component, required) < 0 {
serial-write-cstr(port, " SCI: cap_mint failed")
serial-nl(port)
return -1
}
let virt-load = entry - 32
let image-pages = (image-size + 4095) / 4096
let i = 0
Expand Down
Loading
Loading