diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c208b6..68b8eef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/.gitignore b/.gitignore index f660e3e..c1794bd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ kernel/target/ build/ .pi-subagents/ serial.log +.wrangler/ +mon.sock +s.in +s.out diff --git a/README.md b/README.md index 3f40b04..f06ddb4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/architecture.md b/architecture.md index cab3a25..cecd7c7 100644 --- a/architecture.md +++ b/architecture.md @@ -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`. --- @@ -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 @@ -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. --- @@ -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 | |-----|-----------| @@ -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`. --- @@ -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: @@ -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 diff --git a/components/domain.in b/components/domain.in index 89fe848..368e47c 100644 --- a/components/domain.in +++ b/components/domain.in @@ -10,6 +10,29 @@ 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) @@ -17,7 +40,7 @@ fn domain-init() -> void { 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 @@ -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 @@ -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 } @@ -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 } diff --git a/components/linux.in b/components/linux.in index cfbcb19..3ed1a74 100644 --- a/components/linux.in +++ b/components/linux.in @@ -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 { diff --git a/components/memory.in b/components/memory.in index 7e0219b..b0c5c65 100644 --- a/components/memory.in +++ b/components/memory.in @@ -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 @@ -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) diff --git a/components/network.in b/components/network.in index 854fa7a..9ab02d0 100644 --- a/components/network.in +++ b/components/network.in @@ -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 diff --git a/components/nvme.in b/components/nvme.in index fe6912d..8c2d77a 100644 --- a/components/nvme.in +++ b/components/nvme.in @@ -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) diff --git a/components/object.in b/components/object.in index 938084e..cccc52c 100644 --- a/components/object.in +++ b/components/object.in @@ -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 diff --git a/components/preempt.in b/components/preempt.in index d2742c1..12dcefa 100644 --- a/components/preempt.in +++ b/components/preempt.in @@ -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) diff --git a/components/process.in b/components/process.in index f7ab12d..c8bf516 100644 --- a/components/process.in +++ b/components/process.in @@ -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) @@ -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 diff --git a/components/sci-loader.in b/components/sci-loader.in index 4753cb2..ebb2a97 100644 --- a/components/sci-loader.in +++ b/components/sci-loader.in @@ -36,6 +36,25 @@ var comp-entry-arg: Int = 0 var comp-cap-info-arg: Int = 0 var comp-domain-arg: Int = 0 +fn sci-image-acceptable(entry: Int, image-size: Int) -> Int { + if image-size < 32 || image-size > 0x200000 { + return 0 + } + if entry < 32 { + return 0 + } + return 1 +} + +fn sci-bind-domain-cap(domain: Int, component: Int, required: Int) -> Int { + let cap-idx = cap-mint(component, required) + if cap-idx < 0 { + return -1 + } + domain-set-cap(domain, cap-idx) + return cap-idx +} + fn boot-image-find(kind: Int) -> Int { let table = load64(0x40D0) if table == 0 || load64(table) != BOOT-IMAGE-TABLE-MAGIC { @@ -85,6 +104,11 @@ fn sci-load-runtime-component(port: Int, load-addr: Int, name: Int, grants: Int) let required = load64(load-addr + 8) let entry = load64(load-addr + 16) let image-size = load64(load-addr + 24) + if sci-image-acceptable(entry, image-size) == 0 { + serial-write-cstr(port, " SCI: invalid entry or image-size") + serial-nl(port) + return -1 + } serial-write-cstr(port, " SCI: manifest ok entry 0x") serial-write-hex(port, entry) serial-nl(port) @@ -106,6 +130,11 @@ fn sci-load-runtime-component(port: Int, load-addr: Int, name: Int, grants: Int) serial-nl(port) let component = component-register(name, 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 + 4096 - 1) / 4096 let i = 0 @@ -548,6 +577,11 @@ fn sci-load(port: Int, load-addr: Int) -> Int { let required = load64(load-addr + 8) let entry = load64(load-addr + 16) let image-size = load64(load-addr + 24) + if sci-image-acceptable(entry, image-size) == 0 { + serial-write-cstr(port, " SCI: invalid entry or image-size") + serial-nl(port) + return -1 + } serial-write-cstr(port, " SCI: manifest ok, caps 0x") serial-write-hex(port, required) serial-write-cstr(port, " entry 0x") @@ -567,6 +601,11 @@ fn sci-load(port: Int, load-addr: Int) -> Int { return -1 } let component = component-register("sci-guest", 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 diff --git a/components/shell.in b/components/shell.in index e05d075..1bbf0e5 100644 --- a/components/shell.in +++ b/components/shell.in @@ -1066,6 +1066,42 @@ fn shell(port: Int) -> void { serial-write-cstr(port, " sys-write buf=0 not rejected") serial-nl(port) } + if load64(0x4058) != 0 || load64(0x4060) != 0 { + ok = 0 + serial-write-cstr(port, " cr3 stubs still published") + serial-nl(port) + } + let saved-cap = current-cap + let weak = cap-mint(root-realm, cap-timer()) + current-cap = weak + let probe = "x" + if sys-write(1, probe, 1) != -1 { + ok = 0 + serial-write-cstr(port, " sys-write without serial cap not rejected") + serial-nl(port) + } + if sys-chan-create(4) != 0 { + ok = 0 + serial-write-cstr(port, " sys-chan-create without graph cap not rejected") + serial-nl(port) + } + current-cap = saved-cap + let guest-dom = domain-create() + let guest-comp = component-register("hard-guest", 0, cap-serial()) + if sci-bind-domain-cap(guest-dom, guest-comp, cap-serial()) >= 0 { + domain-switch(guest-dom) + let kg = sys-write(1, 0x200000, 16) + domain-switch(0) + if kg != -1 { + ok = 0 + serial-write-cstr(port, " guest sys-write of kernel globals not rejected") + serial-nl(port) + } + } else { + ok = 0 + serial-write-cstr(port, " guest cap mint failed") + serial-nl(port) + } // DNS parser must reject a truncated message (huge ancount, short len). let dbuf = alloc(64) let di = 0 diff --git a/components/syscall.in b/components/syscall.in index 849db0d..9684266 100644 --- a/components/syscall.in +++ b/components/syscall.in @@ -16,9 +16,15 @@ fn sys-write(fd: Int, buf: Int, len: Int) -> Int { if fd != 1 { return -1 } + if cap-require(cap-serial()) == 0 { + return -1 + } if len <= 0 || len > 4096 || buf == 0 { return -1 } + if domain-current != 0 && buf < COMP-SHARED-VIRT { + return -1 + } let port = com1() let i = 0 while i < len { @@ -34,9 +40,15 @@ fn sys-read(fd: Int, buf: Int, len: Int) -> Int { if fd != 0 { return -1 } + if cap-require(cap-serial()) == 0 { + return -1 + } if len <= 0 || len > 4096 || buf == 0 { return -1 } + if domain-current != 0 && buf < COMP-SHARED-VIRT { + return -1 + } let port = com1() let i = 0 while i < len { @@ -78,22 +90,34 @@ fn sys-getpid() -> Int { // Syscall 5: sys_chan_create(cap) — create a channel with capacity. fn sys-chan-create(cap: Int) -> Int { + if cap-require(cap-graph()) == 0 { + return 0 + } return chan-new(cap) } // Syscall 6: sys_chan_send(ch, msg) — send a message to a channel. fn sys-chan-send(ch: Int, msg: Int) -> void { + if cap-require(cap-graph()) == 0 { + return + } chan-send(ch, msg) return } // Syscall 7: sys_chan_recv(ch) — receive a message from a channel. fn sys-chan-recv(ch: Int) -> Int { + if cap-require(cap-graph()) == 0 { + return -1 + } return chan-recv(ch) } // Syscall 8: sys_chan_close(ch) — close/free a channel. fn sys-chan-close(ch: Int) -> Int { + if cap-require(cap-graph()) == 0 { + return -1 + } chan-heap-free(ch) return 0 } @@ -107,6 +131,9 @@ fn sys-cap-revoke(idx: Int) -> Int { } fn sys-cap-check(idx: Int, rights: Int) -> Int { + if idx != current-cap { + return 0 + } return cap-check(idx, rights) } diff --git a/docs/kernel-audit.md b/docs/kernel-audit.md index 5cd5909..67e20c7 100644 --- a/docs/kernel-audit.md +++ b/docs/kernel-audit.md @@ -20,7 +20,7 @@ Fixed on 2026-08-13 (commit `c57702e`, plus the compiler fix `ea23033` in `../in - **Compiler bug found while fixing**: `in` DCE dropped unused `let` bindings whose initializer was a call, silently eliding side-effecting code (`dsp demo` never ran). Fixed in Inauguration `ea23033` (core_opt: `expr_has_call`). - New gates: `scripts/check-audit-fixes.sh` (hardening assertions), `scripts/check-spdp-composite.sh` (SPDP surface path), `scripts/check-desktop-damage.sh` (moving-window renderer bench). -Open items (not yet fixed): real per-domain page tables (#1), CPL3 execution (#2), capability enforcement at the syscall boundary (#3), TCP FIN/close + ISN randomness, USB HID unification, PCI BAR validation, SparkFS disk-field trust. +Open items (not yet fixed): real per-domain page tables (#1), CPL3 execution (#2), TCP FIN/close + ISN randomness, USB HID unification, PCI BAR validation, SparkFS disk-field trust. Syscall `cap-check` is wired for serial and channel paths; mint/revoke stay `-1`. Domains still clone the 4 GiB identity map. Read files: `kernel-root.in`, `multiboot.asm`, `sci-loader.in`, `supervisor.in`, `process.in`, `domain.in`, `memory.in`, `object.in`, `syscall.in`, `channel.in`, `sched.in`, `preempt.in`, `interrupts.in`, `time.in`, `pci.in`, `nvme.in`, `storage.in`, `net.in`, `netstack.in`, `network.in`, `dhcp.in`, `dns.in`, `usb.in`, `input.in`, `mouse.in`, `display.in` (+`display-standalone.in`), `fs2-{block,file}.in`, `vfs.in`, `posix.in` (execve), `shell.in` (input path), `protocol/display.in`, and 24 check scripts. diff --git a/docs/personalities-roadmap.md b/docs/personalities-roadmap.md index 1420146..e8c039b 100644 --- a/docs/personalities-roadmap.md +++ b/docs/personalities-roadmap.md @@ -1,6 +1,6 @@ # OS Personalities Roadmap -Branch: `feat/personalities` (forked from `main` after translator depth 1–30 Windows + Darwin BSD surface). +Branch: `main` (personalities merged; translator depth 1–30 Windows + Darwin BSD surface). Space does **not** put POSIX / Win32 / XNU in the nanokernel. Personalities are `.in` **translator microservices**: foreign-shaped call numbers map onto Space diff --git a/docs/sparkfs.md b/docs/sparkfs.md index 07d6085..c13e1a0 100644 --- a/docs/sparkfs.md +++ b/docs/sparkfs.md @@ -13,9 +13,8 @@ files, and crash-safe metadata updates. - Long filenames (up to 255 bytes) stored in directory entries. - Block allocation bitmap with free-space tracking. - Metadata journaling for format, create, write, delete, and rename. -- Compatible with the existing NVMe/ATA sector wrappers (`ata_read_sector`, - `ata_write_sector`). The disk is still accessed as 512-byte sectors, but - all on-disk structures are aligned to 4 KiB block boundaries. +- Compatible with the NVMe and memdisk block path. The disk is accessed as + 512-byte sectors; on-disk structures are aligned to 4 KiB block boundaries. ## Non-goals diff --git a/docs/v86-website.md b/docs/v86-website.md index b0f3cd2..041b9d6 100644 --- a/docs/v86-website.md +++ b/docs/v86-website.md @@ -56,6 +56,4 @@ bun run deploy - Website gates: `bun run typecheck` and `bun test` - Website build: `bun run build` (kernel image + static `dist/`) - Browser test: `bun run dev`, then open the local URL and wait for the serial banner -- Live site: `https://space.tsc.hk` - -See [`next-agent-32bit-v86.md`](next-agent-32bit-v86.md) for the original task breakdown and file references. \ No newline at end of file +- Live site: `https://space.tsc.hk` \ No newline at end of file diff --git a/kernel/kernel-root.in b/kernel/kernel-root.in index b1da4c8..c5e7d82 100644 --- a/kernel/kernel-root.in +++ b/kernel/kernel-root.in @@ -196,6 +196,8 @@ fn kernel-entry(mb-info: Int) -> Int { } domain-init() + store64(domain-table + 16, kernel-cap) + current-cap = kernel-cap if boot-quiet == 0 { serial-write-cstr(port, "space: domain subsystem init, ") serial-write-dec-wide(port, domain-count) diff --git a/sci-schema.md b/sci-schema.md index cc511e7..9e90d67 100644 --- a/sci-schema.md +++ b/sci-schema.md @@ -72,7 +72,17 @@ The Inauguration compiler emits component metadata as a JSON sidecar ## Loader Rule -The loader rejects an SCI when: +The loader rejects an SCI when its self-declared required bitmask is not a +subset of the load-path grant mask (`SCI-GUEST-GRANTS` for packed/file guest +SCI; display/input/volume use their own constants). Packed `boot-image-find` +also bounds physical address and size. + +That deny path is metadata. Components run at CPL0 on a cloned 4 GiB identity +map, so a guest can still issue `inb`/`outb` and touch kernel-mapped memory. +Syscall `cap-check` gates serial and channel syscalls; it is not a hardware +boundary. + +The sidecar JSON still records: - a capability is used by code but absent from `capabilities_required` - an import has no granted provider @@ -91,7 +101,7 @@ The loader rejects an SCI when: | Metadata + code in same artifact | ✅ Complete | | Boot image enters `.in`-compiled `kernel_entry` in long mode under QEMU | ✅ Complete | | Loader accepts a binary SCI manifest capability mask before entry | ✅ Complete | -| Loader deny-policy enforcement | ✅ Complete | +| Loader deny-policy enforcement | bitmask vs grant constant; not isolation | ## Future Version Fields diff --git a/scripts/check-all.sh b/scripts/check-all.sh new file mode 100755 index 0000000..ddd992c --- /dev/null +++ b/scripts/check-all.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SPACE_DIR="$(dirname "$SCRIPT_DIR")" +export BUILD_DIR="${BUILD_DIR:-/tmp/space-check-all}" +mkdir -p "$BUILD_DIR" + +bash "$SCRIPT_DIR/check-spdp-protocol.sh" +bash "$SCRIPT_DIR/check-architecture-boundaries.sh" +bash "$SCRIPT_DIR/check-sci-contract.sh" +KERNEL_BIN="${KERNEL_BIN:-$BUILD_DIR/kernel.bin}" +if [ ! -f "$KERNEL_BIN" ]; then + KERNEL_BIN="" +fi +if [ -n "$KERNEL_BIN" ]; then + KERNEL_BIN="$KERNEL_BIN" bash "$SCRIPT_DIR/check-qemu-boot.sh" + KERNEL_BIN="$KERNEL_BIN" bash "$SCRIPT_DIR/check-audit-fixes.sh" +else + bash "$SCRIPT_DIR/check-qemu-boot.sh" + bash "$SCRIPT_DIR/check-audit-fixes.sh" +fi +echo "PASS: check-all" diff --git a/scripts/check-architecture-boundaries.sh b/scripts/check-architecture-boundaries.sh index 951966b..1ba9e65 100644 --- a/scripts/check-architecture-boundaries.sh +++ b/scripts/check-architecture-boundaries.sh @@ -24,4 +24,34 @@ if ! rg -Uq 'fn sys-cap-mint\([^)]*\) -> Int \{\n return -1\n\}' components/sys exit 1 fi +if rg -n 'invoke1\(load64\(0x4060\)' components kernel --glob '*.in'; then + echo "kernel still invokes published cr3_write at 0x4060" >&2 + exit 1 +fi + +if ! rg -q 'cap-require\(cap-serial\(\)\)' components/syscall.in; then + echo "sys-write/sys-read do not consult cap-check" >&2 + exit 1 +fi + +if ! rg -q 'cap-require\(cap-graph\(\)\)' components/syscall.in; then + echo "channel syscalls do not consult cap-check" >&2 + exit 1 +fi + +if rg -n 'required & \(-1 \^ realm-grants\)' components/process.in; then + echo "sci-load-file uses realm-grants instead of SCI-GUEST-GRANTS" >&2 + exit 1 +fi + +if ! rg -q 'clone the 4 GiB identity map' architecture.md; then + echo "architecture.md no longer states that domains clone the identity map" >&2 + exit 1 +fi + +if ! rg -q 'ref: 2e3bf260c9624d5c6127c56febcd0e1938e8d475' .github/workflows/ci.yml; then + echo "CI compiler ref is not pinned to vendor/inauguration" >&2 + exit 1 +fi + echo "PASS: architecture boundaries" diff --git a/scripts/check-qemu-boot.sh b/scripts/check-qemu-boot.sh index a15f796..929ebc8 100755 --- a/scripts/check-qemu-boot.sh +++ b/scripts/check-qemu-boot.sh @@ -10,22 +10,28 @@ IN="$INAUG_DIR/in-cli/target/release/in" SERIAL="$BUILD_DIR/serial.log" FIFO="$BUILD_DIR/serial_in" mkdir -p "$BUILD_DIR" -echo "[1/3] Building compiler..." -[ -x "$IN" ] || cargo build --release -q --manifest-path "$INAUG_DIR/in-cli/Cargo.toml" -echo "[2/3] Assembling trampoline and compiling kernel..." -NASM="${NASM:-nasm}" -"$NASM" -f bin "$SPACE_DIR/boot/multiboot.asm" -o "$BUILD_DIR/trampoline.bin" -[ $(wc -c < "$BUILD_DIR/trampoline.bin") -eq 4096 ] || { echo "trampoline size error" >&2; exit 1; } -"$IN" compile --path "$SPACE_DIR/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" +if [ -n "${KERNEL_BIN:-}" ]; then + echo "[1/3] Using KERNEL_BIN=$KERNEL_BIN" + KERNEL="$KERNEL_BIN" +else + echo "[1/3] Building compiler..." + [ -x "$IN" ] || cargo build --release -q --manifest-path "$INAUG_DIR/in-cli/Cargo.toml" + echo "[2/3] Assembling trampoline and compiling kernel..." + NASM="${NASM:-nasm}" + "$NASM" -f bin "$SPACE_DIR/boot/multiboot.asm" -o "$BUILD_DIR/trampoline.bin" + [ $(wc -c < "$BUILD_DIR/trampoline.bin") -eq 4096 ] || { echo "trampoline size error" >&2; exit 1; } + "$IN" compile --path "$SPACE_DIR/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" + KERNEL="$BUILD_DIR/kernel.bin" +fi echo "[3/3] Booting and checking output..." rm -f "$SERIAL" "$FIFO" mkfifo "$FIFO" # Start QEMU with serial input from the FIFO and output to the log file. # Keep fd 3 open for writing to the FIFO so QEMU's stdin does not see EOF. -qemu-system-x86_64 -kernel "$BUILD_DIR/kernel.bin" -m 512M \ +qemu-system-x86_64 -kernel "$KERNEL" -m 512M \ -rtc base=utc \ -device isa-debug-exit,iobase=0xf4 \ -vga std -serial stdio -display none -no-reboot <"$FIFO" >"$SERIAL" 2>/dev/null & diff --git a/scripts/check-qemu-smoke.sh b/scripts/check-qemu-smoke.sh new file mode 100755 index 0000000..68fd45f --- /dev/null +++ b/scripts/check-qemu-smoke.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SPACE_DIR="$(dirname "$SCRIPT_DIR")" +# shellcheck source=inauguration-dir.sh +source "$SCRIPT_DIR/inauguration-dir.sh" +INAUG_DIR="$(inauguration_dir "$SPACE_DIR")" +BUILD_DIR="${BUILD_DIR:-/tmp/space-qemu-smoke}" +IN="$INAUG_DIR/in-cli/target/release/in" +SERIAL="$BUILD_DIR/serial.log" +FIFO="$BUILD_DIR/serial_in" +mkdir -p "$BUILD_DIR" + +if [ -n "${KERNEL_BIN:-}" ]; then + KERNEL="$KERNEL_BIN" +else + echo "[1/2] Building kernel..." + [ -x "$IN" ] || cargo build --release -q --manifest-path "$INAUG_DIR/in-cli/Cargo.toml" + NASM="${NASM:-nasm}" + "$NASM" -f bin "$SPACE_DIR/boot/multiboot.asm" -o "$BUILD_DIR/trampoline.bin" + "$IN" compile --path "$SPACE_DIR/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" + KERNEL="$BUILD_DIR/kernel.bin" +fi + +echo "[2/2] QEMU smoke (kernel root + interactive shell)..." +command -v qemu-system-x86_64 >/dev/null || { echo "qemu-system-x86_64 not found" >&2; exit 1; } +rm -f "$SERIAL" "$FIFO" +mkfifo "$FIFO" +qemu-system-x86_64 -kernel "$KERNEL" -m 256M \ + -device isa-debug-exit,iobase=0xf4 \ + -vga std -serial stdio -display none -no-reboot <"$FIFO" >"$SERIAL" 2>"$BUILD_DIR/qemu.err" & +QPID=$! +exec 3>"$FIFO" +for _ in $(seq 1 300); do + grep -qF "interactive shell" "$SERIAL" 2>/dev/null && break + kill -0 "$QPID" 2>/dev/null || break + sleep 0.1 +done +echo "halt" >&3 +exec 3>&- +sleep 0.5 +kill "$QPID" 2>/dev/null || true +wait "$QPID" 2>/dev/null || true +rm -f "$FIFO" + +fail= +for m in "kernel root entered" "interactive shell" "space>"; do + if grep -qF "$m" "$SERIAL" 2>/dev/null; then echo " ok: $m" + else echo " MISSING: $m" >&2; fail=1; fi +done +if [ -n "$fail" ]; then + echo "---- serial ----" >&2 + tail -n 80 "$SERIAL" >&2 || true + echo "---- qemu.err ----" >&2 + tail -n 40 "$BUILD_DIR/qemu.err" >&2 || true + echo "FAIL" >&2 + exit 1 +fi +echo "PASS" +exit 0 diff --git a/space-elf-loader b/space-elf-loader deleted file mode 100644 index 991c6e8..0000000 --- a/space-elf-loader +++ /dev/null @@ -1,37 +0,0 @@ -## ELF64 Parser Implementation - -### Created -- `kernel/elf-loader.in` — flat ELF64 parser, single function `fn elf-parse(base: Int) -> Int` - -### Modified -- `kernel/kernel-root.in` — added `import "elf-loader.in"` alongside existing imports - -### Compile Result -``` -success: true -parsed_function_count: 125 (124 + elf-parse) -typed_function_count: 125 -backend_level: owned-native-subset-freestanding -semantic_level: typed-subset -``` - -### What elf-parse Does -1. Validates ELF64 header: magic (0x464C457F), 64-bit, little-endian, x86_64 machine type -2. Reads entry, phoff, phnum from ELF header -3. Creates new domain via `domain_create()` -4. Loops program headers: for each PT_LOAD segment, allocates 4K frames, copies file data byte-by-byte, maps via `domain_map` with translated ELF flags (PF_R/PF_W/PF_X → domain R/W/X) -5. Returns entry virtual address - -### Compile Command -```bash -in compile --path kernel/kernel-root.in --out kernel/kernel-root.o \ - --target native --entry elf-parse \ - --target-triple x86_64-unknown-none --linkage static-lib --json -``` -Note: standalone compile of elf-loader.in fails on unresolved symbols because it is imported into kernel-root.in. - -### Skipped -- BSS zeroing (assumes filesz == memsz for PT_LOAD) — add when ELF binaries use BSS -- Section header parsing — not needed for loading -- Symbol/relocation tables — add for dynamic linking -- `p_filesz` vs `p_memsz` distinction — using p_memsz for copy size diff --git a/todo.md b/todo.md index 006bb66..de5f827 100644 --- a/todo.md +++ b/todo.md @@ -14,7 +14,7 @@ TCP window/congestion: MSS negotiation, slow-start, Go-Back-N retransmit (`check-tcp`). Darwin/Windows M4 surface. Desktop via kernel `display.in` + PS/2; kernel xHCI HID enum works (`usb.in`). Shell: cd/pwd, history up/down, `>`/`|` redirect, nested paths. ELF load above global-data zero region. -18+ maintained checks green on `feat/personalities`. See personalities docs. +18+ maintained checks green on `main`. See personalities docs. ## Phase 1: Storage diff --git a/website/package.json b/website/package.json index 44ed4a0..0f2b7ab 100644 --- a/website/package.json +++ b/website/package.json @@ -26,13 +26,13 @@ "ghostty-web": "^0.4.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "v86": "latest" + "v86": "0.5.445" }, "devDependencies": { "@types/bun": "1.3.14", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "typescript": "^7.0.2", - "wrangler": "latest" + "wrangler": "4.127.0" } }