diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0190e6b..db2f9c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,13 @@ jobs: 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 \ + 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 website: diff --git a/README.md b/README.md index 909bfa2..3f40b04 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ bash scripts/check-terminal-editor.sh # serial editor save test bash scripts/check-desktop-visual.sh # desktop / display visual path bash scripts/check-desktop-damage.sh # moving-window compositor bench bash scripts/check-spdp-composite.sh # SPDP surface-pipeline proof +bash scripts/check-spdp-protocol.sh # SPDP wire-format / parser contract bash scripts/check-audit-fixes.sh # kernel-audit hardening assertions bash scripts/check-linux-elf.sh # Linux ELF personality bash scripts/check-volume-deep-soak.sh # NVMe volume deep soak diff --git a/components/display.in b/components/display.in index 55b1d0a..665b032 100644 --- a/components/display.in +++ b/components/display.in @@ -3,8 +3,6 @@ package space.display import "../protocol/display.in" import "font.in" -const SPDP-OBJ-SURFACE = 5 - interface DisplayDevice { fn map(phys: Int, size: Int) -> void fn present() -> void @@ -2334,6 +2332,7 @@ fn dsp-init() -> void { dsp-surfaces = alloc(DSP-MAX-SURFACES * dsp-surface-size()) dsp-pools = alloc(DSP-MAX-POOLS * 8) // array of pool addresses dsp-pool-sizes = alloc(DSP-MAX-POOLS * 8) // array of pool sizes (bytes) + dsp-msg = alloc(SPDP-MSG-SIZE) let i = 0 while i < DSP-MAX-POOLS { store64(dsp-pools + i * 8, 0) diff --git a/protocol/display.in b/protocol/display.in index 3e5a9c9..cdd469b 100644 --- a/protocol/display.in +++ b/protocol/display.in @@ -1,16 +1,17 @@ // Space Display Protocol — Wayland-inspired compositor wire format. // Objects communicate over channels using a fixed-size message format: -// [obj_id(8)][opcode(8)][argc(8)][args...] -// Each message is exactly 32 bytes (header + up to 3 x 8-byte args). +// [obj_id(8)][opcode(8)][arg0(8)][arg1(8)] +// Each message is exactly 32 bytes (16-byte header + two 8-byte args). const SPDP-MSG-SIZE = 32 -const SPDP-HEADER-SIZE = 24 // obj_id + opcode + argc +const SPDP-HEADER-SIZE = 16 // obj_id + opcode // Object IDs (well-known) const SPDP-OBJ-DISPLAY = 1 // global display object const SPDP-OBJ-COMPOSITOR = 2 // compositor interface const SPDP-OBJ-SHM = 3 // shared memory interface const SPDP-OBJ-SEAT = 4 // input seat +const SPDP-OBJ-SURFACE = 5 // surface object // Display object opcodes (client → server) const SPDP-GET-REGISTRY = 0 // no args @@ -22,8 +23,8 @@ const SPDP-REGISTRY-GLOBAL = 1 // arg0: obj_id, arg1: interface_addr // Compositor opcodes const SPDP-COMPOSITOR-CREATE-SURFACE = 0 // returns surface_id -const SPDP-SURFACE-ATTACH = 1 // arg0: surface_id, arg1: buffer_id -const SPDP-SURFACE-DAMAGE = 2 // arg0: surface_id, arg1: x/w packed +const SPDP-SURFACE-ATTACH = 1 // arg0: surface_id, arg1: pool_id<<32 | offset +const SPDP-SURFACE-DAMAGE = 2 // arg0: surface_id, arg1: x<<48|y<<32|w<<16|h const SPDP-SURFACE-COMMIT = 3 // arg0: surface_id const SPDP-SURFACE-DESTROY = 4 // arg0: surface_id @@ -45,8 +46,8 @@ const SPDP-SEAT-KEYBOARD-KEY = 4 // arg0: time, arg1: key/state packed // Surface life cycle: // 1. Client sends COMPOSITOR_CREATE_SURFACE → receives surface_id // 2. Client creates an SHM pool via SHM_CREATE_POOL -// 3. Client sends SURFACE_ATTACH(surface_id, buffer_id) to associate buffer -// 4. Client sends SURFACE_DAMAGE(surface_id, x, y, w, h) to mark dirty region +// 3. Client sends SURFACE_ATTACH(surface_id, pool_id<<32 | offset) +// 4. Client sends SURFACE_DAMAGE(surface_id, x<<48|y<<32|w<<16|h) to mark dirty region // 5. Client sends SURFACE_COMMIT(surface_id) to present // 6. Compositor renders the surface, sends frame callback // diff --git a/scripts/check-spdp-protocol.sh b/scripts/check-spdp-protocol.sh new file mode 100755 index 0000000..2d99518 --- /dev/null +++ b/scripts/check-spdp-protocol.sh @@ -0,0 +1,138 @@ +"exec" "python3" "$0" "$@" +# check-spdp-protocol.sh — Host-side SPDP wire-format + compositor parse checks. +# No QEMU / compiler. Run: bash scripts/check-spdp-protocol.sh +"""Validate protocol/display.in against the compositor's 32-byte SPDP frames. + +The kernel parser in components/display.in (dsp-run / dsp-handle-msg) is the +source of truth: four little-endian u64 words, no argc field. +""" +import os, re, struct, sys + +passed, failed = 0, 0 + +def check(label, ok): + global passed, failed + if ok: + print(f" ok: {label}") + passed += 1 + else: + print(f" FAIL: {label}") + failed += 1 + +def parse_consts(text): + found = {} + for name, val in re.findall(r"^const (SPDP-[A-Z0-9-]+) = (\d+)", text, re.M): + found[name] = int(val) + return found + +def encode(obj_id, opcode, arg0, arg1): + return struct.pack("> 32) & 0xFFFFFFFF, packed & 0xFFFFFFFF + +def unpack_geometry(packed): + return ( + (packed >> 48) & 0xFFFF, + (packed >> 32) & 0xFFFF, + (packed >> 16) & 0xFFFF, + packed & 0xFFFF, + ) + +def main(): + here = os.path.dirname(os.path.abspath(__file__)) + space = os.path.abspath(os.path.join(here, "..")) + proto_path = os.path.join(space, "protocol", "display.in") + display_path = os.path.join(space, "components", "display.in") + ci_path = os.path.join(space, ".github", "workflows", "ci.yml") + + proto = open(proto_path).read() + display = open(display_path).read() + ci = open(ci_path).read() + consts = parse_consts(proto) + + print("[1/4] Protocol constants and frame layout...") + check("SPDP-MSG-SIZE == 32", consts.get("SPDP-MSG-SIZE") == 32) + check("SPDP-HEADER-SIZE == 16 (obj_id + opcode, no argc)", + consts.get("SPDP-HEADER-SIZE") == 16) + check("header + two args == message size", + consts.get("SPDP-HEADER-SIZE", 0) + 16 == consts.get("SPDP-MSG-SIZE", -1)) + check("protocol does not reserve an argc word", + "argc" not in proto.lower()) + + for name, val in [ + ("SPDP-OBJ-DISPLAY", 1), + ("SPDP-OBJ-COMPOSITOR", 2), + ("SPDP-OBJ-SHM", 3), + ("SPDP-OBJ-SEAT", 4), + ("SPDP-OBJ-SURFACE", 5), + ("SPDP-COMPOSITOR-CREATE-SURFACE", 0), + ("SPDP-SURFACE-ATTACH", 1), + ("SPDP-SURFACE-DAMAGE", 2), + ("SPDP-SURFACE-COMMIT", 3), + ("SPDP-SHM-CREATE-POOL", 0), + ]: + check(f"{name} == {val}", consts.get(name) == val) + + print("[2/4] Encode/decode packed arguments...") + surface = consts.get("SPDP-OBJ-SURFACE", 5) + attach_op = consts.get("SPDP-SURFACE-ATTACH", 1) + frame = encode(surface, attach_op, 7, pack_attach(3, 0x1000)) + check("encoded frame is 32 bytes", len(frame) == 32) + obj_id, opcode, arg0, arg1 = decode(frame) + check("decode obj_id/opcode/arg0", obj_id == surface and opcode == attach_op and arg0 == 7) + pool, off = unpack_attach(arg1) + check("SURFACE-ATTACH packs pool<<32 | offset", pool == 3 and off == 0x1000) + + gx, gy, gw, gh = unpack_geometry(pack_geometry(100, 100, 64, 64)) + check("SURFACE-DAMAGE packs x<<48|y<<32|w<<16|h", + (gx, gy, gw, gh) == (100, 100, 64, 64)) + + # A client that followed the old argc-at-offset-16 layout would put argc in + # the word the compositor reads as arg0. That must not be the spec. + wrong = struct.pack("