From 72caf911b103f250d32e024845da8316fef513f2 Mon Sep 17 00:00:00 2001 From: Markus Maiwald Date: Sun, 15 Feb 2026 18:01:10 +0100 Subject: [PATCH] feat: recover M3-M4 untracked files, add .gitignore - Add ARM64 support files never committed to monorepo: entry_aarch64.zig, gic.zig, virtio_mmio.zig, littlefs_hal.zig, linker_aarch64.ld, linker_user_aarch64.ld, run_aarch64.sh - Add build scripts: build_full.sh, build_nim.sh, build_lwip.sh - Add Libertaria LWF adapters: lwf_adapter.zig, lwf_membrane.zig - Add LittleFS bridge: lfs_bridge.nim, lfs_rumpk.h - Add freestanding headers: math.h, stdio.h, stdlib.h - Add .gitignore blocking build artifacts and internal dirs --- .gitignore | 54 ++ apps/linker_user_aarch64.ld | 44 ++ boot/linker_aarch64.ld | 54 ++ build_full.sh | 125 ++++ build_lwip.sh | 61 ++ build_nim.sh | 126 ++++ core/fs/lfs_bridge.nim | 129 ++++ hal/entry_aarch64.zig | 1060 ++++++++++++++++++++++++++++++ hal/gic.zig | 169 +++++ hal/littlefs_hal.zig | 362 ++++++++++ hal/virtio_mmio.zig | 268 ++++++++ libs/libertaria/lwf_adapter.zig | 512 +++++++++++++++ libs/libertaria/lwf_membrane.zig | 300 +++++++++ libs/membrane/include/math.h | 16 + libs/membrane/include/stdio.h | 13 + libs/membrane/include/stdlib.h | 10 + run_aarch64.sh | 86 +++ vendor/littlefs/lfs_rumpk.h | 127 ++++ zig-cc-wrapper.sh | 2 + 19 files changed, 3518 insertions(+) create mode 100644 .gitignore create mode 100644 apps/linker_user_aarch64.ld create mode 100644 boot/linker_aarch64.ld create mode 100755 build_full.sh create mode 100755 build_lwip.sh create mode 100755 build_nim.sh create mode 100644 core/fs/lfs_bridge.nim create mode 100644 hal/entry_aarch64.zig create mode 100644 hal/gic.zig create mode 100644 hal/littlefs_hal.zig create mode 100644 hal/virtio_mmio.zig create mode 100644 libs/libertaria/lwf_adapter.zig create mode 100644 libs/libertaria/lwf_membrane.zig create mode 100644 libs/membrane/include/math.h create mode 100644 libs/membrane/include/stdio.h create mode 100644 libs/membrane/include/stdlib.h create mode 100755 run_aarch64.sh create mode 100644 vendor/littlefs/lfs_rumpk.h create mode 100755 zig-cc-wrapper.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df970c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Build artifacts +build/ +zig-out/ +.zig-cache/ +nimcache/ +*.o +*.a +*.elf +*.img +*.bin +*.log + +# Kernel build intermediates +build_full.log +current_run.elf +kernel_cache.elf +kernel_final.elf +abi.o + +# Nim cache +build/nimcache/ +build/init_nimcache/ +build/lwip_objs/ + +# InitRD build outputs (regenerated) +build/sysro/ +build/initrd.tar +build/embed_initrd.S +build/init +build/head.o +build/head.S +build/head_user.o +build/head_user.S +build/disk.img +build/disk_aarch64.img +build/clib_user.o +build/dummy.c + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Agent / internal (must never appear) +.agent/ +.claude/ +.kiro/ +competitors/ diff --git a/apps/linker_user_aarch64.ld b/apps/linker_user_aarch64.ld new file mode 100644 index 0000000..3250668 --- /dev/null +++ b/apps/linker_user_aarch64.ld @@ -0,0 +1,44 @@ +/* Memory Layout — ARM64 Cellular Memory (M3.3): + * User RAM: 0x48000000 - 0x4FFFFFFF (128MB) + * Stack starts at 0x4BFFFFF0 and grows down + * QEMU virt: -m 512M ensures valid physical backing + */ +MEMORY +{ + RAM (rwx) : ORIGIN = 0x48000000, LENGTH = 128M +} + +SECTIONS +{ + . = 0x48000000; + + .text : { + *(.text._start) + *(.text) + *(.text.*) + } > RAM + + .rodata : { + *(.rodata) + *(.rodata.*) + } > RAM + + .data : { + *(.data) + *(.data.*) + } > RAM + + .nexus.manifest : { + KEEP(*(.nexus.manifest)) + } > RAM + + .bss : { + . = ALIGN(8); + __bss_start = .; + *(.bss) + *(.bss.*) + *(COMMON) + . = ALIGN(8); + __bss_end = .; + } > RAM +} diff --git a/boot/linker_aarch64.ld b/boot/linker_aarch64.ld new file mode 100644 index 0000000..6462ea2 --- /dev/null +++ b/boot/linker_aarch64.ld @@ -0,0 +1,54 @@ +/* Rumpk Linker Script (AArch64) + * For QEMU virt machine (ARM64) + * Load address: 0x40080000 (QEMU -kernel default for virt) + */ + +ENTRY(_start) + +SECTIONS +{ + . = 0x40080000; + PROVIDE(__kernel_vbase = .); + PROVIDE(__kernel_pbase = .); + + .text : { + *(.text._start) + *(.text*) + } + + .rodata : { + *(.rodata*) + } + + .data : { + . = ALIGN(16); + *(.sdata*) + *(.sdata.*) + *(.data*) + } + + .initrd : { + _initrd_start = .; + KEEP(*(.initrd)) + _initrd_end = .; + } + + .bss : { + __bss_start = .; + *(.bss*) + *(COMMON) + __bss_end = .; + } + + .stack (NOLOAD) : { + . = ALIGN(16); + . += 0x100000; /* 1MB Stack */ + PROVIDE(__stack_top = .); + } + + /DISCARD/ : { + *(.comment) + *(.note*) + *(.eh_frame*) + } +} diff --git a/build_full.sh b/build_full.sh new file mode 100755 index 0000000..fbbca63 --- /dev/null +++ b/build_full.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env zsh +set -e + +ARCH=${1:-riscv64} + +# Architecture-specific settings +if [ "$ARCH" = "aarch64" ]; then + NIM_CPU="arm64" + ZIG_TARGET="aarch64-freestanding-none" + ZIG_CPU="baseline" + LINKER_SCRIPT="apps/linker_user_aarch64.ld" + BUILD_FLAG="-Darch=aarch64" + echo "=== Building NipBox Userland (aarch64) ===" +else + NIM_CPU="riscv64" + ZIG_TARGET="riscv64-freestanding-none" + ZIG_CPU="sifive_u54" + LINKER_SCRIPT="apps/linker_user.ld" + BUILD_FLAG="" + echo "=== Building NipBox Userland (riscv64) ===" +fi + +# Compile Nim sources to C +nim c --cpu:${NIM_CPU} --os:any --compileOnly --mm:arc --opt:size \ + --stackTrace:off --lineDir:off --nomain --nimcache:build/init_nimcache \ + -d:noSignalHandler -d:RUMPK_USER -d:nimAllocPagesViaMalloc -d:NIPBOX_LITE \ + npl/nipbox/nipbox.nim + +# Compile Nim-generated C (check if files exist first) +# Skip net_glue (needs LwIP headers not available in userland build) +EXTRA_CC_FLAGS="" +if [ "$ARCH" = "riscv64" ]; then + EXTRA_CC_FLAGS="-mcmodel=medany" +fi + +if ls build/init_nimcache/*.c 1> /dev/null 2>&1; then + for f in build/init_nimcache/*.c; do + case "$f" in + *net_glue*) echo " [skip] $f (LwIP dependency)"; continue ;; + esac + zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \ + -fno-sanitize=all -fno-vectorize \ + -I/usr/lib/nim/lib -Icore -Ilibs/membrane -Ilibs/membrane/include \ + -include string.h \ + -Os -c "$f" -o "${f%.c}.o" + done +fi + +# Compile clib +zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \ + -fno-sanitize=all \ + -DNO_SYS=1 -DOMIT_EXIT -DRUMPK_USER -Ilibs/membrane/include -c libs/membrane/clib.c -o build/clib_user.o + +# Create startup assembly +if [ "$ARCH" = "aarch64" ]; then + cat > build/head_user.S << 'EOF' +.section .text._start +.global _start +_start: + bl NimMain +1: wfi + b 1b +EOF +else + cat > build/head_user.S << 'EOF' +.section .text._start +.global _start +_start: + .option push + .option norelax + 1:auipc gp, %pcrel_hi(__global_pointer$) + addi gp, gp, %pcrel_lo(1b) + .option pop + call NimMain + 1: wfi + j 1b +EOF +fi + +zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \ + -fno-sanitize=all \ + -c build/head_user.S -o build/head_user.o + +# Link init +zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} -nostdlib \ + -fno-sanitize=all \ + -T ${LINKER_SCRIPT} -Wl,--gc-sections \ + build/head_user.o build/init_nimcache/*.o build/clib_user.o \ + -o build/init + +echo "✓ NipBox binary built (${ARCH})" +file build/init + +# Create initrd +mkdir -p build/sysro/bin +cp build/init build/sysro/init +if [ "$ARCH" = "riscv64" ] && [ -f vendor/mksh/mksh.elf ]; then + cp vendor/mksh/mksh.elf build/sysro/bin/mksh +fi +cd build/sysro +tar --format=ustar -cf ../initrd.tar * +cd ../.. + +# Embed initrd +cat > build/embed_initrd.S << EOF +.section .rodata +.global _initrd_start +.global _initrd_end +.align 4 +_initrd_start: + .incbin "$(pwd)/build/initrd.tar" +_initrd_end: +EOF + +zig cc -target ${ZIG_TARGET} -mcpu=${ZIG_CPU} ${EXTRA_CC_FLAGS} \ + -c build/embed_initrd.S -o build/initrd.o + +cp build/initrd.tar hal/initrd.tar + +# Build kernel +rm -f zig-out/lib/librumpk_hal.a +zig build ${BUILD_FLAG} + +echo "=== BUILD COMPLETE (${ARCH}) ===" +ls -lh build/init zig-out/bin/rumpk.elf diff --git a/build_lwip.sh b/build_lwip.sh new file mode 100755 index 0000000..e6597f3 --- /dev/null +++ b/build_lwip.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env zsh +# Build LwIP as a pure C library without Zig runtime dependencies + +set -e + +mkdir -p build/lwip_objs +rm -f build/lwip_objs/*.o 2>/dev/null || true + +echo "Building LwIP..." + +# Compile each source file +compile() { + local src=$1 + local obj="build/lwip_objs/$(basename ${src%.c}.o)" + echo " $src" + zig cc -target riscv64-freestanding-none -mcpu=sifive_u54 -mcmodel=medany \ + -Os -fno-sanitize=all \ + -DNO_SYS=1 -Icore -Ilibs/membrane -Ilibs/membrane/include \ + -Ilibs/membrane/external/lwip/src/include \ + -c "$src" -o "$obj" +} + +# Core sources +compile "libs/membrane/external/lwip/src/core/init.c" +compile "libs/membrane/external/lwip/src/core/def.c" +compile "libs/membrane/external/lwip/src/core/dns.c" +compile "libs/membrane/external/lwip/src/core/inet_chksum.c" +compile "libs/membrane/external/lwip/src/core/ip.c" +compile "libs/membrane/external/lwip/src/core/mem.c" +compile "libs/membrane/external/lwip/src/core/memp.c" +compile "libs/membrane/external/lwip/src/core/netif.c" +compile "libs/membrane/external/lwip/src/core/pbuf.c" +compile "libs/membrane/external/lwip/src/core/raw.c" +compile "libs/membrane/external/lwip/src/core/sys.c" +compile "libs/membrane/external/lwip/src/core/tcp.c" +compile "libs/membrane/external/lwip/src/core/tcp_in.c" +compile "libs/membrane/external/lwip/src/core/tcp_out.c" +compile "libs/membrane/external/lwip/src/core/timeouts.c" +compile "libs/membrane/external/lwip/src/core/udp.c" + +# IPv4 sources +compile "libs/membrane/external/lwip/src/core/ipv4/autoip.c" +compile "libs/membrane/external/lwip/src/core/ipv4/dhcp.c" +compile "libs/membrane/external/lwip/src/core/ipv4/etharp.c" +compile "libs/membrane/external/lwip/src/core/ipv4/icmp.c" +compile "libs/membrane/external/lwip/src/core/ipv4/ip4.c" +compile "libs/membrane/external/lwip/src/core/ipv4/ip4_addr.c" +compile "libs/membrane/external/lwip/src/core/ipv4/ip4_frag.c" + +# Netif sources +compile "libs/membrane/external/lwip/src/netif/ethernet.c" + +# SysArch +compile "libs/membrane/sys_arch.c" + +echo "Creating liblwip.a..." +mkdir -p zig-out/lib +rm -f zig-out/lib/liblwip.a +(cd build/lwip_objs && ar rcs ../../zig-out/lib/liblwip.a *.o) +echo "Done! liblwip.a created at zig-out/lib/liblwip.a" +ls -lh zig-out/lib/liblwip.a diff --git a/build_nim.sh b/build_nim.sh new file mode 100755 index 0000000..d6afd90 --- /dev/null +++ b/build_nim.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# ============================================================================ +# Rumpk Nim Kernel Build — nim → C → .o (cross-compiled for target arch) +# ============================================================================ +# Usage: +# ./build_nim.sh # Default: riscv64 +# ./build_nim.sh riscv64 # RISC-V 64-bit +# ./build_nim.sh aarch64 # ARM64 +# ./build_nim.sh x86_64 # AMD64 +# +# This script: +# 1. Invokes nim c --compileOnly to generate C from Nim +# 2. Cross-compiles each .c to .o using zig cc +# 3. Outputs to build/nimcache/*.o (consumed by build.zig) +# ============================================================================ +set -euo pipefail +cd "$(dirname "$0")" + +ARCH="${1:-riscv64}" + +# ---- Validate architecture ---- +case "$ARCH" in + riscv64) + ZIG_TARGET="riscv64-freestanding-none" + ZIG_CPU="-mcpu=sifive_u54" + ZIG_MODEL="-mcmodel=medany" + NIM_CPU="riscv64" + ;; + aarch64) + ZIG_TARGET="aarch64-freestanding-none" + ZIG_CPU="" + ZIG_MODEL="-fno-vectorize" + NIM_CPU="arm64" + ;; + x86_64) + ZIG_TARGET="x86_64-freestanding-none" + ZIG_CPU="" + ZIG_MODEL="-mcmodel=kernel" + NIM_CPU="amd64" + ;; + *) + echo "ERROR: Unknown architecture '$ARCH'" + echo "Supported: riscv64, aarch64, x86_64" + exit 1 + ;; +esac + +NIMCACHE="build/nimcache" + +echo "=== Rumpk Nim Build: $ARCH ===" +echo " Target: $ZIG_TARGET" +echo " Output: $NIMCACHE/" + +# ---- Step 1: Nim → C ---- +echo "" +echo "[1/2] nim c --compileOnly core/kernel.nim" + +nim c \ + --cpu:"$NIM_CPU" \ + --os:any \ + --compileOnly \ + --mm:arc \ + --opt:size \ + --stackTrace:off \ + --lineDir:off \ + --nomain \ + --nimcache:"$NIMCACHE" \ + -d:noSignalHandler \ + -d:RUMPK_KERNEL \ + -d:nimAllocPagesViaMalloc \ + core/kernel.nim + +C_COUNT=$(ls -1 "$NIMCACHE"/*.c 2>/dev/null | wc -l) +echo " Generated $C_COUNT C files" + +# ---- Step 2: C → .o (zig cc cross-compile) ---- +echo "" +echo "[2/2] zig cc → $ZIG_TARGET" + +COMPILED=0 +FAILED=0 + +for cfile in "$NIMCACHE"/*.c; do + [ -f "$cfile" ] || continue + ofile="${cfile%.c}.o" + + # Skip if .o is newer than .c (incremental) + if [ -f "$ofile" ] && [ "$ofile" -nt "$cfile" ]; then + continue + fi + + if zig cc \ + -target "$ZIG_TARGET" \ + $ZIG_CPU \ + $ZIG_MODEL \ + -fno-sanitize=all \ + -fvisibility=default \ + -I/usr/lib/nim/lib \ + -Icore \ + -Icore/include \ + -Ilibs/membrane \ + -Ilibs/membrane/include \ + -Ilibs/membrane/external/lwip/src/include \ + -Os \ + -c "$cfile" \ + -o "$ofile" 2>/dev/null; then + COMPILED=$((COMPILED + 1)) + else + echo " FAIL: $(basename "$cfile")" + FAILED=$((FAILED + 1)) + fi +done + +O_COUNT=$(ls -1 "$NIMCACHE"/*.o 2>/dev/null | wc -l) + +echo "" +echo "=== Result ===" +echo " Arch: $ARCH" +echo " C files: $C_COUNT" +echo " Compiled: $COMPILED (incremental skip: $((O_COUNT - COMPILED)))" +echo " Objects: $O_COUNT" +if [ "$FAILED" -gt 0 ]; then + echo " FAILED: $FAILED" + exit 1 +fi +echo " Status: OK" diff --git a/core/fs/lfs_bridge.nim b/core/fs/lfs_bridge.nim new file mode 100644 index 0000000..8cd2eaf --- /dev/null +++ b/core/fs/lfs_bridge.nim @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: LSL-1.0 +# Copyright (c) 2026 Markus Maiwald +# Stewardship: Self Sovereign Society Foundation +# +# This file is part of the Nexus Sovereign Core. +# See legal/LICENSE_SOVEREIGN.md for license terms. + +## Rumpk Layer 1: LittleFS Bridge +## +## Nim FFI wrapper for the Zig-side LittleFS HAL (littlefs_hal.zig). +## Provides the API that VFS delegates to for /nexus mount point. +## +## All calls cross the Nim→Zig→C boundary: +## Nim (this file) → Zig (littlefs_hal.zig) → C (lfs.c) → VirtIO-Block + +# --- FFI imports from littlefs_hal.zig (exported as C ABI) --- +proc nexus_lfs_format(): int32 {.importc, cdecl.} +proc nexus_lfs_mount(): int32 {.importc, cdecl.} +proc nexus_lfs_unmount(): int32 {.importc, cdecl.} +proc nexus_lfs_open(path: cstring, flags: int32): int32 {.importc, cdecl.} +proc nexus_lfs_read(handle: int32, buf: pointer, size: uint32): int32 {.importc, cdecl.} +proc nexus_lfs_write(handle: int32, buf: pointer, size: uint32): int32 {.importc, cdecl.} +proc nexus_lfs_close(handle: int32): int32 {.importc, cdecl.} +proc nexus_lfs_seek(handle: int32, off: int32, whence: int32): int32 {.importc, cdecl.} +proc nexus_lfs_size(handle: int32): int32 {.importc, cdecl.} +proc nexus_lfs_remove(path: cstring): int32 {.importc, cdecl.} +proc nexus_lfs_mkdir(path: cstring): int32 {.importc, cdecl.} +proc nexus_lfs_is_mounted(): int32 {.importc, cdecl.} + +# --- LFS open flags (match lfs.h) --- +const + LFS_O_RDONLY* = 1'i32 + LFS_O_WRONLY* = 2'i32 + LFS_O_RDWR* = 3'i32 + LFS_O_CREAT* = 0x0100'i32 + LFS_O_EXCL* = 0x0200'i32 + LFS_O_TRUNC* = 0x0400'i32 + LFS_O_APPEND* = 0x0800'i32 + +# --- LFS seek flags --- +const + LFS_SEEK_SET* = 0'i32 + LFS_SEEK_CUR* = 1'i32 + LFS_SEEK_END* = 2'i32 + +# --- Public API for VFS --- + +proc lfs_mount_fs*(): bool = + ## Mount the LittleFS filesystem. Auto-formats on first boot. + return nexus_lfs_mount() == 0 + +proc lfs_unmount_fs*(): bool = + return nexus_lfs_unmount() == 0 + +proc lfs_format_fs*(): bool = + return nexus_lfs_format() == 0 + +proc lfs_is_mounted*(): bool = + return nexus_lfs_is_mounted() != 0 + +proc lfs_open_file*(path: cstring, flags: int32): int32 = + ## Open a file. Returns handle >= 0 on success, < 0 on error. + return nexus_lfs_open(path, flags) + +proc lfs_read_file*(handle: int32, buf: pointer, size: uint32): int32 = + ## Read from file. Returns bytes read or negative error. + return nexus_lfs_read(handle, buf, size) + +proc lfs_write_file*(handle: int32, buf: pointer, size: uint32): int32 = + ## Write to file. Returns bytes written or negative error. + return nexus_lfs_write(handle, buf, size) + +proc lfs_close_file*(handle: int32): int32 = + return nexus_lfs_close(handle) + +proc lfs_seek_file*(handle: int32, off: int32, whence: int32): int32 = + return nexus_lfs_seek(handle, off, whence) + +proc lfs_file_size*(handle: int32): int32 = + return nexus_lfs_size(handle) + +proc lfs_remove_path*(path: cstring): int32 = + return nexus_lfs_remove(path) + +proc lfs_mkdir_path*(path: cstring): int32 = + return nexus_lfs_mkdir(path) + +# --- Convenience: VFS-compatible read/write (path-based, like SFS) --- + +proc lfs_vfs_read*(path: cstring, buf: pointer, max_len: int): int = + ## Read entire file into buffer. Returns bytes read or -1. + let h = nexus_lfs_open(path, LFS_O_RDONLY) + if h < 0: return -1 + let n = nexus_lfs_read(h, buf, uint32(max_len)) + discard nexus_lfs_close(h) + if n < 0: return -1 + return int(n) + +proc lfs_vfs_write*(path: cstring, buf: pointer, len: int) = + ## Write buffer to file (create/truncate). + let h = nexus_lfs_open(path, LFS_O_WRONLY or LFS_O_CREAT or LFS_O_TRUNC) + if h < 0: return + discard nexus_lfs_write(h, buf, uint32(len)) + discard nexus_lfs_close(h) + +proc lfs_vfs_read_at*(path: cstring, buf: pointer, count: uint64, + offset: uint64): int64 = + ## Read `count` bytes starting at `offset`. Returns bytes read. + let h = nexus_lfs_open(path, LFS_O_RDONLY) + if h < 0: return -1 + if offset > 0: + discard nexus_lfs_seek(h, int32(offset), LFS_SEEK_SET) + let n = nexus_lfs_read(h, buf, uint32(count)) + discard nexus_lfs_close(h) + if n < 0: return -1 + return int64(n) + +proc lfs_vfs_write_at*(path: cstring, buf: pointer, count: uint64, + offset: uint64): int64 = + ## Write `count` bytes at `offset`. Returns bytes written. + let flags = LFS_O_WRONLY or LFS_O_CREAT + let h = nexus_lfs_open(path, flags) + if h < 0: return -1 + if offset > 0: + discard nexus_lfs_seek(h, int32(offset), LFS_SEEK_SET) + let n = nexus_lfs_write(h, buf, uint32(count)) + discard nexus_lfs_close(h) + if n < 0: return -1 + return int64(n) diff --git a/hal/entry_aarch64.zig b/hal/entry_aarch64.zig new file mode 100644 index 0000000..bbfbc7f --- /dev/null +++ b/hal/entry_aarch64.zig @@ -0,0 +1,1060 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Rumpk HAL: AArch64 Entry Point (Sovereign Trap Architecture) +//! +//! This is the hardware floor for ARM64. Sets up exception vectors, +//! GIC, Generic Timer, and PL011 UART before handing off to Nim. +//! +//! SAFETY: Runs in bare-metal EL1 with identity mapping (no MMU in M3.1). + +const std = @import("std"); +const uart = @import("uart.zig"); +const gic = @import("gic.zig"); +const uart_input = @import("uart_input.zig"); + +// ========================================================= +// L1 Kernel Logic (Nim FFI) +// ========================================================= + +extern fn k_handle_syscall(nr: usize, a0: usize, a1: usize, a2: usize) usize; +extern fn k_handle_exception(cause: usize, pc: usize, addr: usize) void; +extern fn k_check_deferred_yield() void; +extern fn kmain() void; +extern fn NimMain() void; +extern fn hal_surface_init() void; + +// ========================================================= +// Trap Frame (34 registers * 8 = 272 bytes, 16-byte aligned = 288) +// ========================================================= + +const TrapFrame = extern struct { + // x0-x30 (31 GPRs) + x: [31]usize, + // SP_EL0 (user stack pointer) + sp_el0: usize, + // Exception Link Register (return address) + elr_el1: usize, + // Saved Program Status Register + spsr_el1: usize, + // ESR_EL1 (Exception Syndrome) + esr_el1: usize, + // FAR_EL1 (Fault Address) + far_el1: usize, +}; + +// ========================================================= +// Exception Vector Table +// ========================================================= +// ARM64 requires 16 entries, each 128 bytes (32 instructions), aligned to 2048. +// Layout: +// [0x000] Current EL, SP0: Sync / IRQ / FIQ / SError +// [0x200] Current EL, SPx: Sync / IRQ / FIQ / SError <- kernel traps +// [0x400] Lower EL, AArch64: Sync / IRQ / FIQ / SError <- userland traps +// [0x600] Lower EL, AArch32: Sync / IRQ / FIQ / SError <- unused + +// Vector table is built at runtime by install_vectors_asm() — no comptime needed. + +// ========================================================= +// Vector Handlers (Assembly Trampolines) +// ========================================================= + +// Shared context save/restore macro as inline asm. +// Saves x0-x30, SP_EL0, ELR_EL1, SPSR_EL1, ESR_EL1, FAR_EL1 onto kernel stack. +// Total frame: 36 * 8 = 288 bytes (16-byte aligned). + +fn save_context() callconv(.naked) void { + asm volatile ( + // Allocate trap frame (288 bytes = 36 * 8) + \\ sub sp, sp, #288 + // Save x0-x30 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + // Save SP_EL0 + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + // Save ELR_EL1 + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + // Save SPSR_EL1 + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + // Save ESR_EL1 + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + // Save FAR_EL1 + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + // x0 = frame pointer (sp) + \\ mov x0, sp + \\ ret + ); +} + +fn restore_context() callconv(.naked) void { + asm volatile ( + // Restore ELR_EL1 + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + // Restore SPSR_EL1 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + // Restore SP_EL0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + // Restore x0-x30 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + // Deallocate frame + \\ add sp, sp, #288 + \\ eret + ); +} + +// Sync exception from current EL (kernel) +export fn vector_sync_handler() callconv(.naked) void { + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// IRQ from current EL (kernel) +export fn vector_irq_handler() callconv(.naked) void { + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// Sync exception from lower EL (userland SVC) +export fn vector_sync_lower() callconv(.naked) void { + // Same save/restore/eret pattern + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// IRQ from lower EL (userland interrupted) +export fn vector_irq_lower() callconv(.naked) void { + asm volatile ( + \\ sub sp, sp, #288 + \\ stp x0, x1, [sp, #0] + \\ stp x2, x3, [sp, #16] + \\ stp x4, x5, [sp, #32] + \\ stp x6, x7, [sp, #48] + \\ stp x8, x9, [sp, #64] + \\ stp x10, x11, [sp, #80] + \\ stp x12, x13, [sp, #96] + \\ stp x14, x15, [sp, #112] + \\ stp x16, x17, [sp, #128] + \\ stp x18, x19, [sp, #144] + \\ stp x20, x21, [sp, #160] + \\ stp x22, x23, [sp, #176] + \\ stp x24, x25, [sp, #192] + \\ stp x26, x27, [sp, #208] + \\ stp x28, x29, [sp, #224] + \\ str x30, [sp, #240] + \\ mrs x0, sp_el0 + \\ str x0, [sp, #248] + \\ mrs x0, elr_el1 + \\ str x0, [sp, #256] + \\ mrs x0, spsr_el1 + \\ str x0, [sp, #264] + \\ mrs x0, esr_el1 + \\ str x0, [sp, #272] + \\ mrs x0, far_el1 + \\ str x0, [sp, #280] + \\ mov x0, sp + \\ bl rss_trap_handler + \\ ldr x0, [sp, #256] + \\ msr elr_el1, x0 + \\ ldr x0, [sp, #264] + \\ msr spsr_el1, x0 + \\ ldr x0, [sp, #248] + \\ msr sp_el0, x0 + \\ ldp x0, x1, [sp, #0] + \\ ldp x2, x3, [sp, #16] + \\ ldp x4, x5, [sp, #32] + \\ ldp x6, x7, [sp, #48] + \\ ldp x8, x9, [sp, #64] + \\ ldp x10, x11, [sp, #80] + \\ ldp x12, x13, [sp, #96] + \\ ldp x14, x15, [sp, #112] + \\ ldp x16, x17, [sp, #128] + \\ ldp x18, x19, [sp, #144] + \\ ldp x20, x21, [sp, #160] + \\ ldp x22, x23, [sp, #176] + \\ ldp x24, x25, [sp, #192] + \\ ldp x26, x27, [sp, #208] + \\ ldp x28, x29, [sp, #224] + \\ ldr x30, [sp, #240] + \\ add sp, sp, #288 + \\ eret + ); +} + +// ========================================================= +// Trap Handler (Zig Logic) +// ========================================================= + +// ESR_EL1 Exception Class codes +const EC_SVC64: u6 = 0x15; // SVC in AArch64 +const EC_DATA_ABORT_LOWER: u6 = 0x24; +const EC_DATA_ABORT_SAME: u6 = 0x25; +const EC_INSN_ABORT_LOWER: u6 = 0x20; +const EC_INSN_ABORT_SAME: u6 = 0x21; + +var trap_depth: usize = 0; + +export fn rss_trap_handler(frame: *TrapFrame) void { + trap_depth += 1; + if (trap_depth > 3) { + uart.print("[Trap] Infinite Loop Detected. Halting.\n"); + while (true) { + asm volatile ("wfe"); + } + } + defer trap_depth -= 1; + + const esr = frame.esr_el1; + const ec: u6 = @truncate((esr >> 26) & 0x3F); + + // Determine if this came from IRQ vector or Sync vector + // by checking if ESR indicates an interrupt (EC=0 from IRQ vector) + // Actually, IRQ vectors call us too — but ESR won't have useful EC for IRQ. + // We use a simple heuristic: if called from IRQ vector, EC will be 0 typically. + // Better approach: check GIC for pending IRQs first. + + // Try to claim an IRQ — if one is pending, this is an interrupt + const irq = gic.gic_claim(); + if (!gic.is_spurious(irq)) { + // Interrupt path + if (irq == gic.TIMER_IRQ) { + // Timer interrupt: acknowledge and disable until rescheduled + timer_ack(); + k_check_deferred_yield(); + } else if (irq == gic.UART_IRQ) { + uart_input.poll_input(); + } else if (irq >= gic.VIRTIO_MMIO_IRQ_BASE and irq < gic.VIRTIO_MMIO_IRQ_BASE + 32) { + // VirtIO MMIO device interrupt — poll net driver + const virtio_net = @import("virtio_net.zig"); + virtio_net.virtio_net_poll(); + } + gic.gic_complete(irq); + return; + } + + // Synchronous exception path + if (ec == EC_SVC64) { + // Syscall: x8 = number, x0-x2 = args (ARM64 convention) + const nr = frame.x[8]; + const a0 = frame.x[0]; + const a1 = frame.x[1]; + const a2 = frame.x[2]; + + // Advance PC past SVC instruction + frame.elr_el1 += 4; + + const ret = k_handle_syscall(nr, a0, a1, a2); + frame.x[0] = ret; + } else if (ec == EC_DATA_ABORT_LOWER or ec == EC_DATA_ABORT_SAME or + ec == EC_INSN_ABORT_LOWER or ec == EC_INSN_ABORT_SAME) + { + uart.print("\n[Trap] Abort! EC:"); + uart.print_hex(@as(usize, ec)); + uart.print(" PC:"); + uart.print_hex(frame.elr_el1); + uart.print(" FAR:"); + uart.print_hex(frame.far_el1); + uart.print("\n"); + k_handle_exception(@as(usize, ec), frame.elr_el1, frame.far_el1); + while (true) { + asm volatile ("wfe"); + } + } else { + uart.print("\n[Trap] Unhandled EC:"); + uart.print_hex(@as(usize, ec)); + uart.print(" ESR:"); + uart.print_hex(esr); + uart.print(" PC:"); + uart.print_hex(frame.elr_el1); + uart.print("\n"); + } +} + +// ========================================================= +// ARM Generic Timer +// ========================================================= + +var cntfrq: u64 = 0; // Timer frequency (read at init) +var ns_per_tick_x16: u64 = 0; // (1e9 / freq) * 16 for fixed-point + +fn timer_init() void { + // Read timer frequency + cntfrq = asm volatile ("mrs %[ret], cntfrq_el0" + : [ret] "=r" (-> u64), + ); + + if (cntfrq == 0) { + // Fallback: QEMU virt typically uses 62.5 MHz + cntfrq = 62_500_000; + } + + // Precompute ns_per_tick * 16 for fixed-point math + // ns_per_tick = 1_000_000_000 / cntfrq + // We use * 16 to avoid floating point: (1e9 * 16) / cntfrq + ns_per_tick_x16 = (1_000_000_000 * 16) / cntfrq; + + // Disable timer initially + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0)), + ); +} + +fn timer_ack() void { + // Disable timer (mask) to prevent re-firing until rescheduled + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0x2)), // IMASK=1, ENABLE=0 + ); +} + +export fn rumpk_timer_now_ns() u64 { + const cnt: u64 = asm volatile ("mrs %[ret], cntpct_el0" + : [ret] "=r" (-> u64), + ); + // Convert to nanoseconds using precomputed fixed-point + // ns = cnt * ns_per_tick = cnt * (ns_per_tick_x16 / 16) + return (cnt * ns_per_tick_x16) >> 4; +} + +export fn rumpk_timer_set_ns(interval_ns: u64) void { + if (interval_ns == std.math.maxInt(u64)) { + // Disable timer + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0x2)), // IMASK=1 + ); + return; + } + + // Convert ns to ticks: ticks = ns * cntfrq / 1e9 + const ticks = (interval_ns * cntfrq) / 1_000_000_000; + + // Set countdown value and enable + asm volatile ("msr cntp_tval_el0, %[val]" + : + : [val] "r" (ticks), + ); + asm volatile ("msr cntp_ctl_el0, %[val]" + : + : [val] "r" (@as(u64, 0x1)), // ENABLE=1, IMASK=0 + ); +} + +// ========================================================= +// Identity Map (MMU Setup) +// ========================================================= +// ARM64 without MMU treats all memory as Device-nGnRnE, which requires +// strict alignment. We set up a minimal identity map with: +// MAIR index 0: Device-nGnRnE (0x00) — for MMIO +// MAIR index 1: Normal Write-Back Cacheable (0xFF) — for RAM +// Using 1GB block descriptors at Level 1 (only need L0 + L1 tables). + +// Page table storage (must be 4096-byte aligned) +// 39-bit VA (T0SZ=25) starts walk at L1 — no L0 needed +var l1_table: [512]u64 align(4096) = [_]u64{0} ** 512; + +fn setup_identity_map() void { + // MAIR_EL1: index 0 = Device-nGnRnE, index 1 = Normal WB Cacheable + const MAIR_VAL: u64 = 0xFF_00; // attr1=0xFF (Normal WB), attr0=0x00 (Device) + asm volatile ("msr mair_el1, %[val]" + : + : [val] "r" (MAIR_VAL), + ); + + // TCR_EL1: 4KB granule, 36-bit PA, T0SZ=25 (39-bit VA = 512GB) + // IPS=0b010 (40-bit PA), TG0=0b00 (4KB), SH0=0b11 (Inner Shareable), + // ORGN0=0b01 (WB Cacheable), IRGN0=0b01 (WB Cacheable), T0SZ=25 + const TCR_VAL: u64 = (0b010 << 32) | // IPS: 40-bit PA + (0b00 << 14) | // TG0: 4KB granule + (0b11 << 12) | // SH0: Inner Shareable + (0b01 << 10) | // ORGN0: Write-Back Cacheable + (0b01 << 8) | // IRGN0: Write-Back Cacheable + 25; // T0SZ: 39-bit VA space + asm volatile ("msr tcr_el1, %[val]" + : + : [val] "r" (TCR_VAL), + ); + + // With T0SZ=25, VA is 39 bits → translation starts at L1 (no L0 needed). + // L1 entry [38:30] = 9 bits → 512 entries, each 1GB block. + // TTBR0_EL1 points directly at the L1 table. + + // Block descriptor: addr[47:30] | AF | SH | AP | AttrIdx | Block(0b01) + const BLOCK_DEVICE: u64 = (1 << 10) | // AF (Access Flag) + (0b00 << 8) | // SH: Non-shareable (Device) + (0b00 << 6) | // AP: EL1 RW + (0b00 << 2) | // AttrIdx: 0 (Device-nGnRnE) + 0x1; // Block descriptor + + const BLOCK_NORMAL: u64 = (1 << 10) | // AF (Access Flag) + (0b11 << 8) | // SH: Inner Shareable + (0b00 << 6) | // AP: EL1 RW + (0b01 << 2) | // AttrIdx: 1 (Normal Cacheable) + 0x1; // Block descriptor + + // GB 0 (0x00000000-0x3FFFFFFF): Device (UART, GIC, etc.) + l1_table[0] = (0x00000000) | BLOCK_DEVICE; + // GB 1 (0x40000000-0x7FFFFFFF): Normal RAM (QEMU virt RAM) + l1_table[1] = (0x40000000) | BLOCK_NORMAL; + // GB 2 (0x80000000-0xBFFFFFFF): Device (high MMIO) + l1_table[2] = (0x80000000) | BLOCK_DEVICE; + // GB 3 (0xC0000000-0xFFFFFFFF): Device + l1_table[3] = (0xC0000000) | BLOCK_DEVICE; + + // Set TTBR0_EL1 to point at L1 table directly (39-bit VA starts at L1) + const l1_addr = @intFromPtr(&l1_table); + asm volatile ("msr ttbr0_el1, %[val]" + : + : [val] "r" (l1_addr), + ); + + // Invalidate TLB + asm volatile ("tlbi vmalle1"); + asm volatile ("dsb sy"); + asm volatile ("isb"); + + // Enable MMU + caches in SCTLR_EL1 + var sctlr: u64 = 0; + asm volatile ("mrs %[out], sctlr_el1" + : [out] "=r" (sctlr), + ); + sctlr |= (1 << 0); // M: Enable MMU + sctlr |= (1 << 2); // C: Enable data cache + sctlr |= (1 << 12); // I: Enable instruction cache + sctlr &= ~@as(u64, 1 << 1); // A: Disable alignment check + asm volatile ("msr sctlr_el1, %[val]" + : + : [val] "r" (sctlr), + ); + asm volatile ("isb"); +} + +// ========================================================= +// Entry Point +// ========================================================= + +// SAFETY(Stack): Memory is immediately used by _start before any read. +export var stack_bytes: [64 * 1024]u8 align(16) = undefined; + +export fn aarch64_init() void { + // 1. Initialize UART (PL011) + uart.init(); + uart.print("[Rumpk L0] aarch64_init reached\n"); + + // Set up identity-mapped page tables so RAM has Normal memory type. + // Without MMU, ARM64 uses Device memory which requires strict alignment. + setup_identity_map(); + uart.print("[Rumpk L0] Identity map + MMU enabled\n"); + + // 2. Initialize GIC + gic.gic_init(); + gic.gic_enable_timer_irq(); + uart.print("[Rumpk L0] GICv2 initialized\n"); + + // 3. Initialize Generic Timer + timer_init(); + uart.print("[Rumpk L0] Generic Timer initialized (freq="); + uart.print_hex(cntfrq); + uart.print(")\n"); + + // 4. Install exception vectors + // We write the vector table with proper branch instructions at runtime + install_vectors_asm(); + uart.print("[Rumpk L0] Exception vectors installed\n"); + + // 5. Enable IRQs (clear DAIF.I bit) + asm volatile ("msr daifclr, #0x2"); // Clear IRQ mask + + uart.print("[Rumpk ARM64] Handing off to Nim L1...\n"); + + // 6. Initialize Nim runtime and enter kernel + NimMain(); + kmain(); + rumpk_halt(); +} + +/// Install exception vectors using runtime assembly +/// This writes proper branch instructions into the vector table +fn install_vectors_asm() void { + // Set VBAR_EL1 to point at our vector handler functions + // We use a simpler approach: write a small vector table in a static buffer + // with branch instructions to our Zig handler functions. + + // The vector table entries need to branch to our handlers. + // ARM64 exception vectors: each entry is 128 bytes (0x80). + // We write `b ` at the start of each entry. + + // For the entries we care about: + // 0x200: Current EL SPx Sync -> vector_sync_handler + // 0x280: Current EL SPx IRQ -> vector_irq_handler + // 0x400: Lower EL Sync -> vector_sync_lower + // 0x480: Lower EL IRQ -> vector_irq_lower + + // We need the vector table to be 2048-byte aligned. + // Use our static vector_table_runtime buffer. + const table_addr = @intFromPtr(&vector_table_runtime); + + // Fill with WFE (halt) as default + const wfe_insn: u32 = 0xD503205F; // WFE + var i: usize = 0; + while (i < 2048) : (i += 4) { + const ptr: *volatile u32 = @ptrFromInt(table_addr + i); + ptr.* = wfe_insn; + } + + // Write branch instructions to our handlers + write_branch_to(table_addr + 0x200, @intFromPtr(&vector_sync_handler)); + write_branch_to(table_addr + 0x280, @intFromPtr(&vector_irq_handler)); + write_branch_to(table_addr + 0x400, @intFromPtr(&vector_sync_lower)); + write_branch_to(table_addr + 0x480, @intFromPtr(&vector_irq_lower)); + + // Set VBAR_EL1 + asm volatile ("msr vbar_el1, %[vbar]" + : + : [vbar] "r" (table_addr), + ); + asm volatile ("isb"); +} + +/// Runtime-writable vector table (2048 bytes, 2048-byte aligned) +var vector_table_runtime: [2048]u8 align(2048) = [_]u8{0} ** 2048; + +/// Write a branch instruction at `from` that jumps to `target` +fn write_branch_to(from: usize, target: usize) void { + // ARM64 B instruction: 0x14000000 | (imm26) + // imm26 is a signed offset in 4-byte units + const offset_bytes: i64 = @as(i64, @intCast(target)) - @as(i64, @intCast(from)); + const offset_words: i32 = @intCast(@divExact(offset_bytes, 4)); + const imm26: u32 = @as(u32, @bitCast(offset_words)) & 0x03FFFFFF; + const insn: u32 = 0x14000000 | imm26; + const ptr: *volatile u32 = @ptrFromInt(from); + ptr.* = insn; +} + +// ========================================================= +// HAL Exports (Contract with L1 Nim Kernel) +// ========================================================= + +export fn hal_console_write(ptr: [*]const u8, len: usize) void { + uart.write_bytes(ptr[0..len]); +} + +export fn console_read() c_int { + if (uart_input.read_byte()) |b| { + return @as(c_int, b); + } + return -1; +} + +export fn console_poll() void { + uart_input.poll_input(); +} + +export fn debug_uart_lsr() u8 { + return uart.get_lsr(); +} + +export fn uart_print_hex(value: u64) void { + uart.print_hex(value); +} + +export fn uart_print_hex8(value: u8) void { + uart.print_hex8(value); +} + +export fn hal_io_init() void { + uart.init(); + hal_surface_init(); + + // Initialize VirtIO block storage (MMIO transport) + const virtio_block = @import("virtio_block.zig"); + virtio_block.init(); +} + +export fn hal_panic(msg: [*:0]const u8) callconv(.c) noreturn { + uart.print("[HAL PANIC] "); + uart.print(std.mem.span(msg)); + uart.print("\n"); + rumpk_halt(); +} + +export fn rumpk_halt() noreturn { + uart.print("[Rumpk ARM64] Halting.\n"); + while (true) { + asm volatile ("wfe"); + } +} + +export fn hal_kexec(entry: u64, dtb: u64) noreturn { + _ = entry; + _ = dtb; + uart.print("[HAL] kexec not implemented on ARM64\n"); + rumpk_halt(); +} + +// ========================================================= +// Page Table Infrastructure (M3.3 — 4KB Granule, 39-bit VA) +// ========================================================= +// The identity map above uses 1GB block descriptors for early boot. +// For user isolation we need 4KB page granularity (L1→L2→L3 walk). + +const PAGE_SIZE: u64 = 4096; +const PAGE_SHIFT: u6 = 12; +const ENTRIES_PER_TABLE: usize = 512; + +// ARM64 descriptor bits +const DESC_VALID: u64 = 1 << 0; +const DESC_TABLE: u64 = 0b11; // L1/L2 table pointer +const DESC_PAGE: u64 = 0b11; // L3 page descriptor +const DESC_AF: u64 = 1 << 10; // Access Flag +const DESC_SH_ISH: u64 = 0b11 << 8; // Inner Shareable +const DESC_AP_RW_EL1: u64 = 0b00 << 6; // EL1 RW, EL0 no access +const DESC_AP_RW_ALL: u64 = 0b01 << 6; // EL1+EL0 RW +const DESC_UXN: u64 = @as(u64, 1) << 54; // Unprivileged Execute Never +const DESC_PXN: u64 = @as(u64, 1) << 53; // Privileged Execute Never +const ATTR_DEVICE: u64 = 0b00 << 2; // AttrIdx=0 (Device-nGnRnE) +const ATTR_NORMAL: u64 = 0b01 << 2; // AttrIdx=1 (Normal WB Cacheable) + +const DRAM_BASE: u64 = 0x40000000; + +// Bump allocator for page tables (8MB pool) +var pt_alloc_base: u64 = 0; +var pt_alloc_offset: u64 = 0; +const PT_POOL_SIZE: u64 = 8 * 1024 * 1024; + +fn pt_init_allocator(base: u64) void { + pt_alloc_base = base; + pt_alloc_offset = 0; +} + +/// Allocate one zeroed 4KB-aligned page table +fn pt_alloc() ?[*]u64 { + if (pt_alloc_offset + PAGE_SIZE > PT_POOL_SIZE) { + uart.print("[MM] Page table pool exhausted!\n"); + return null; + } + const addr = pt_alloc_base + pt_alloc_offset; + pt_alloc_offset += PAGE_SIZE; + + // Zero all 512 entries + const table: [*]volatile u64 = @ptrFromInt(addr); + for (0..ENTRIES_PER_TABLE) |i| { + table[i] = 0; + } + return @ptrFromInt(addr); +} + +/// Map a single 4KB page: walk L1→L2→L3, allocating intermediate tables +fn map_page(root: [*]u64, va: u64, pa: u64, attrs: u64) void { + // 39-bit VA with 4KB granule: + // L1 index = VA[38:30] (9 bits) + // L2 index = VA[29:21] (9 bits) + // L3 index = VA[20:12] (9 bits) + const l1_idx = (va >> 30) & 0x1FF; + const l2_idx = (va >> 21) & 0x1FF; + const l3_idx = (va >> 12) & 0x1FF; + + // L1 → L2 table + const l1_entry = root[l1_idx]; + const l2_table: [*]u64 = if (l1_entry & DESC_VALID != 0) + @ptrFromInt(l1_entry & 0x0000FFFFFFFFF000) + else blk: { + const new_l2 = pt_alloc() orelse return; + root[l1_idx] = @intFromPtr(new_l2) | DESC_TABLE; + break :blk new_l2; + }; + + // L2 → L3 table + const l2_entry = l2_table[l2_idx]; + const l3_table: [*]u64 = if (l2_entry & DESC_VALID != 0) + @ptrFromInt(l2_entry & 0x0000FFFFFFFFF000) + else blk: { + const new_l3 = pt_alloc() orelse return; + l2_table[l2_idx] = @intFromPtr(new_l3) | DESC_TABLE; + break :blk new_l3; + }; + + // L3 page descriptor: pa[47:12] | attrs | DESC_PAGE (0b11) + l3_table[l3_idx] = (pa & 0x0000FFFFFFFFF000) | attrs | DESC_PAGE; +} + +/// Map a range of pages (va and pa must be page-aligned) +fn map_range(root: [*]u64, va_start: u64, pa_start: u64, size: u64, attrs: u64) void { + var offset: u64 = 0; + while (offset < size) : (offset += PAGE_SIZE) { + map_page(root, va_start + offset, pa_start + offset, attrs); + } +} + +// ========================================================= +// HAL Userland Entry (EL1 → EL0 via eret) +// ========================================================= + +export fn hal_enter_userland(entry: u64, systable: u64, sp: u64) callconv(.c) void { + // SPSR_EL1 = 0 → return to EL0t (M[3:0]=0b0000), DAIF clear (IRQs enabled) + const spsr: u64 = 0; + + asm volatile ( + \\ msr spsr_el1, %[spsr] + \\ msr elr_el1, %[entry] + \\ msr sp_el0, %[sp] + \\ mov x0, %[systable] + \\ eret + : + : [spsr] "r" (spsr), + [entry] "r" (entry), + [sp] "r" (sp), + [systable] "r" (systable), + ); +} + +// ========================================================= +// Memory Management (M3.3 — Full Page Tables) +// ========================================================= + +extern fn kprint(s: [*:0]const u8) void; +extern fn kprint_hex(n: u64) void; + +var kernel_ttbr0: u64 = 0; + +export fn mm_init() callconv(.c) void { + // Page table pool at DRAM_BASE + 240MB (same offset as RISC-V) + pt_init_allocator(DRAM_BASE + 240 * 1024 * 1024); +} + +export fn mm_enable_kernel_paging() callconv(.c) void { + // Identity map is already set up by setup_identity_map() using 1GB blocks. + // Store current TTBR0 for later restore after worker map switches. + asm volatile ("mrs %[out], ttbr0_el1" + : [out] "=r" (kernel_ttbr0), + ); +} + +export fn mm_get_kernel_satp() callconv(.c) u64 { + return kernel_ttbr0; +} + +export fn mm_create_worker_map( + stack_base: u64, + stack_size: u64, + packet_addr: u64, + phys_base: u64, + region_size: u64, +) callconv(.c) u64 { + const root = pt_alloc() orelse return 0; + + kprint("[MM] Cellular Map: phys_base="); + kprint_hex(phys_base); + kprint(" size="); + kprint_hex(region_size); + kprint("\n"); + + // Kernel attributes: EL1 RW, Normal cacheable, no EL0 access + const kern_attrs = DESC_AF | DESC_SH_ISH | DESC_AP_RW_EL1 | ATTR_NORMAL; + + // User attributes: EL1+EL0 RW, Normal cacheable + const user_attrs = DESC_AF | DESC_SH_ISH | DESC_AP_RW_ALL | ATTR_NORMAL; + + // Device attributes: EL1 only, Device memory + const dev_attrs = DESC_AF | DESC_AP_RW_EL1 | ATTR_DEVICE; + + // Shared attributes: EL1+EL0 RW, Normal cacheable (for SysTable/ION rings) + const shared_attrs = DESC_AF | DESC_SH_ISH | DESC_AP_RW_ALL | ATTR_NORMAL | DESC_UXN; + + // 1. Kernel memory (0x40000000–0x48000000 = 128MB): EL1 only + // Allows kernel trap handlers to execute while worker map is active + map_range(root, DRAM_BASE, DRAM_BASE, 128 * 1024 * 1024, kern_attrs); + + // 2. User cell (identity mapped): EL0 accessible + // Init: VA 0x48000000 → PA 0x48000000 (64MB) + // Child: VA 0x48000000 → PA phys_base (varies) + const user_va_base = DRAM_BASE + 128 * 1024 * 1024; // 0x48000000 + map_range(root, user_va_base, phys_base, region_size, user_attrs); + + // 3. MMIO devices: EL1 only (kernel handles I/O) + map_range(root, 0x09000000, 0x09000000, PAGE_SIZE, dev_attrs); // PL011 UART + map_range(root, 0x08000000, 0x08000000, 0x20000, dev_attrs); // GICv2 + map_range(root, 0x0a000000, 0x0a000000, 0x200 * 32, dev_attrs); // VirtIO MMIO + + // 4. SysTable + ION rings: EL0 RW (256KB = 64 pages) + map_range(root, packet_addr, packet_addr, 64 * PAGE_SIZE, shared_attrs); + + // 5. Optional kernel stack mapping (if stack_base != 0) + if (stack_base != 0) { + map_range(root, stack_base, stack_base, stack_size, user_attrs); + } + + kprint("[MM] Worker map created successfully\n"); + + // Return TTBR0 value (physical address of root table) + // ARM64 TTBR has no mode bits like RISC-V SATP — just the address + return @intFromPtr(root); +} + +export fn mm_activate_satp(satp_val: u64) callconv(.c) void { + asm volatile ("msr ttbr0_el1, %[val]" + : + : [val] "r" (satp_val), + ); + asm volatile ("isb"); + asm volatile ("tlbi vmalle1"); + asm volatile ("dsb sy"); + asm volatile ("isb"); +} + +export fn mm_debug_check_va(va: u64) callconv(.c) void { + kprint("[MM] Inspecting VA: "); + kprint_hex(va); + kprint("\n"); + + // Read current TTBR0 + var ttbr0: u64 = 0; + asm volatile ("mrs %[out], ttbr0_el1" + : [out] "=r" (ttbr0), + ); + const root: [*]const u64 = @ptrFromInt(ttbr0 & 0x0000FFFFFFFFF000); + + const l1_idx = (va >> 30) & 0x1FF; + const l1_entry = root[l1_idx]; + kprint(" L1["); + kprint_hex(l1_idx); + kprint("]: "); + kprint_hex(l1_entry); + if (l1_entry & DESC_VALID == 0) { + kprint(" (Invalid)\n"); + return; + } + if (l1_entry & 0b10 == 0) { + kprint(" (Block)\n"); + return; + } + kprint(" (Table)\n"); + + const l2: [*]const u64 = @ptrFromInt(l1_entry & 0x0000FFFFFFFFF000); + const l2_idx = (va >> 21) & 0x1FF; + const l2_entry = l2[l2_idx]; + kprint(" L2["); + kprint_hex(l2_idx); + kprint("]: "); + kprint_hex(l2_entry); + if (l2_entry & DESC_VALID == 0) { + kprint(" (Invalid)\n"); + return; + } + if (l2_entry & 0b10 == 0) { + kprint(" (Block)\n"); + return; + } + kprint(" (Table)\n"); + + const l3: [*]const u64 = @ptrFromInt(l2_entry & 0x0000FFFFFFFFF000); + const l3_idx = (va >> 12) & 0x1FF; + const l3_entry = l3[l3_idx]; + kprint(" L3["); + kprint_hex(l3_idx); + kprint("]: "); + kprint_hex(l3_entry); + kprint("\n"); +} + +// VirtIO drivers now provided by virtio_net.zig and virtio_block.zig via abi.zig imports diff --git a/hal/gic.zig b/hal/gic.zig new file mode 100644 index 0000000..59e95da --- /dev/null +++ b/hal/gic.zig @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Rumpk Layer 0: GICv2 Driver (ARM64) +//! +//! Minimal Generic Interrupt Controller v2 for QEMU virt machine. +//! Handles interrupt enable, claim, and complete for timer and device IRQs. +//! +//! SAFETY: All register accesses use volatile pointers to MMIO regions. + +// ========================================================= +// GICv2 MMIO Base Addresses (QEMU virt machine) +// ========================================================= + +const GICD_BASE: usize = 0x08000000; // Distributor +const GICC_BASE: usize = 0x08010000; // CPU Interface + +// ========================================================= +// Distributor Registers (GICD) +// ========================================================= + +const GICD_CTLR: usize = 0x000; // Control +const GICD_TYPER: usize = 0x004; // Type (read-only) +const GICD_ISENABLER: usize = 0x100; // Set-Enable (banked per 32 IRQs) +const GICD_ICENABLER: usize = 0x180; // Clear-Enable +const GICD_ISPENDR: usize = 0x200; // Set-Pending +const GICD_ICPENDR: usize = 0x280; // Clear-Pending +const GICD_IPRIORITYR: usize = 0x400; // Priority (byte-accessible) +const GICD_ITARGETSR: usize = 0x800; // Target (byte-accessible) +const GICD_ICFGR: usize = 0xC00; // Configuration + +// ========================================================= +// CPU Interface Registers (GICC) +// ========================================================= + +const GICC_CTLR: usize = 0x000; // Control +const GICC_PMR: usize = 0x004; // Priority Mask +const GICC_IAR: usize = 0x00C; // Interrupt Acknowledge +const GICC_EOIR: usize = 0x010; // End of Interrupt + +// ========================================================= +// IRQ Numbers (QEMU virt) +// ========================================================= + +/// Non-Secure Physical Timer PPI +pub const TIMER_IRQ: u32 = 30; + +/// UART PL011 (SPI #1 = IRQ 33) +pub const UART_IRQ: u32 = 33; + +/// VirtIO MMIO IRQ base (SPI #16 = IRQ 48) +/// QEMU virt assigns SPIs 48..79 to MMIO slots 0..31 +pub const VIRTIO_MMIO_IRQ_BASE: u32 = 48; + +// Spurious interrupt ID +const SPURIOUS_IRQ: u32 = 1023; + +// ========================================================= +// MMIO Helpers +// ========================================================= + +fn gicd_read(offset: usize) u32 { + const ptr: *volatile u32 = @ptrFromInt(GICD_BASE + offset); + return ptr.*; +} + +fn gicd_write(offset: usize, val: u32) void { + const ptr: *volatile u32 = @ptrFromInt(GICD_BASE + offset); + ptr.* = val; +} + +fn gicc_read(offset: usize) u32 { + const ptr: *volatile u32 = @ptrFromInt(GICC_BASE + offset); + return ptr.*; +} + +fn gicc_write(offset: usize, val: u32) void { + const ptr: *volatile u32 = @ptrFromInt(GICC_BASE + offset); + ptr.* = val; +} + +// ========================================================= +// Public API +// ========================================================= + +/// Initialize GICv2 distributor and CPU interface. +pub fn gic_init() void { + // 1. Disable distributor during setup + gicd_write(GICD_CTLR, 0); + + // 2. Set all SPIs to lowest priority (0xFF) and target CPU 0 + // PPIs (0-31) are banked per-CPU, handled separately + const typer = gicd_read(GICD_TYPER); + const it_lines = (typer & 0x1F) + 1; // Number of 32-IRQ groups + var i: usize = 1; // Skip group 0 (SGIs/PPIs - banked) + while (i < it_lines) : (i += 1) { + // Disable all SPIs + gicd_write(GICD_ICENABLER + i * 4, 0xFFFFFFFF); + // Set priority to 0xA0 (low but not lowest) + var j: usize = 0; + while (j < 8) : (j += 1) { + gicd_write(GICD_IPRIORITYR + (i * 32 + j * 4), 0xA0A0A0A0); + } + // Target CPU 0 for all SPIs + j = 0; + while (j < 8) : (j += 1) { + gicd_write(GICD_ITARGETSR + (i * 32 + j * 4), 0x01010101); + } + } + + // 3. Configure PPI priorities (group 0, banked) + // Timer IRQ 30: priority 0x20 (high) + const timer_prio_reg = GICD_IPRIORITYR + (TIMER_IRQ / 4) * 4; + const timer_prio_shift: u5 = @intCast((TIMER_IRQ % 4) * 8); + var prio_val = gicd_read(timer_prio_reg); + prio_val &= ~(@as(u32, 0xFF) << timer_prio_shift); + prio_val |= @as(u32, 0x20) << timer_prio_shift; + gicd_write(timer_prio_reg, prio_val); + + // 4. Enable distributor (Group 0 + Group 1) + gicd_write(GICD_CTLR, 0x3); + + // 5. Configure CPU interface + gicc_write(GICC_PMR, 0xFF); // Accept all priorities + gicc_write(GICC_CTLR, 0x1); // Enable CPU interface +} + +/// Enable a specific interrupt in the distributor. +pub fn gic_enable_irq(irq: u32) void { + const reg = GICD_ISENABLER + (irq / 32) * 4; + const bit: u5 = @intCast(irq % 32); + gicd_write(reg, @as(u32, 1) << bit); +} + +/// Disable a specific interrupt in the distributor. +pub fn gic_disable_irq(irq: u32) void { + const reg = GICD_ICENABLER + (irq / 32) * 4; + const bit: u5 = @intCast(irq % 32); + gicd_write(reg, @as(u32, 1) << bit); +} + +/// Acknowledge an interrupt (read IAR). Returns IRQ number or SPURIOUS_IRQ. +pub fn gic_claim() u32 { + return gicc_read(GICC_IAR) & 0x3FF; +} + +/// Signal end of interrupt processing. +pub fn gic_complete(irq: u32) void { + gicc_write(GICC_EOIR, irq); +} + +/// Check if a claimed IRQ is spurious. +pub fn is_spurious(irq: u32) bool { + return irq >= SPURIOUS_IRQ; +} + +/// Enable the NS Physical Timer interrupt (IRQ 30). +pub fn gic_enable_timer_irq() void { + gic_enable_irq(TIMER_IRQ); +} + +/// Enable a VirtIO MMIO slot interrupt in the GIC. +pub fn gic_enable_virtio_mmio_irq(slot: u32) void { + gic_enable_irq(VIRTIO_MMIO_IRQ_BASE + slot); +} diff --git a/hal/littlefs_hal.zig b/hal/littlefs_hal.zig new file mode 100644 index 0000000..f3f1b92 --- /dev/null +++ b/hal/littlefs_hal.zig @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_SOVEREIGN.md for license terms. + +//! Rumpk Layer 0: LittleFS ↔ VirtIO-Block HAL +//! +//! Translates LittleFS block operations into VirtIO-Block sector I/O. +//! Exports C-ABI functions for Nim L1 to call: nexus_lfs_mount, nexus_lfs_format, +//! nexus_lfs_open, nexus_lfs_read, nexus_lfs_write, nexus_lfs_close, etc. +//! +//! Block geometry: +//! - LFS block size: 4096 bytes (8 sectors) +//! - Sector size: 512 bytes (VirtIO standard) +//! - 32MB disk: 8192 blocks + +const BLOCK_SIZE: u32 = 4096; +const SECTOR_SIZE: u32 = 512; +const SECTORS_PER_BLOCK: u32 = BLOCK_SIZE / SECTOR_SIZE; +const BLOCK_COUNT: u32 = 8192; // 32MB / 4096 +const CACHE_SIZE: u32 = 512; +const LOOKAHEAD_SIZE: u32 = 64; + +// --- VirtIO-Block FFI (from virtio_block.zig) --- +extern fn virtio_blk_read(sector: u64, buf: [*]u8) void; +extern fn virtio_blk_write(sector: u64, buf: [*]const u8) void; + +// --- Kernel print (from Nim L1 kernel.nim, exported as C ABI) --- +extern fn kprint(s: [*:0]const u8) void; + +// --- LittleFS C types (must match lfs.h layout exactly) --- +// We use opaque pointers and only declare what we need for the config struct. + +const LfsConfig = extern struct { + context: ?*anyopaque, + read: *const fn (*LfsConfig, u32, u32, ?*anyopaque, u32) callconv(.c) i32, + prog: *const fn (*LfsConfig, u32, u32, ?*anyopaque, u32) callconv(.c) i32, + erase: *const fn (*LfsConfig, u32) callconv(.c) i32, + sync: *const fn (*LfsConfig) callconv(.c) i32, + read_size: u32, + prog_size: u32, + block_size: u32, + block_count: u32, + block_cycles: i32, + cache_size: u32, + lookahead_size: u32, + compact_thresh: u32, + read_buffer: ?*anyopaque, + prog_buffer: ?*anyopaque, + lookahead_buffer: ?*anyopaque, + name_max: u32, + file_max: u32, + attr_max: u32, + metadata_max: u32, + inline_max: u32, +}; + +// Opaque LittleFS types — we let lfs.c manage the internals +const LfsT = opaque {}; +const LfsFileT = opaque {}; +const LfsInfo = opaque {}; + +// --- LittleFS C API (linked from lfs.o) --- +extern fn lfs_format(lfs: *LfsT, config: *LfsConfig) callconv(.c) i32; +extern fn lfs_mount(lfs: *LfsT, config: *LfsConfig) callconv(.c) i32; +extern fn lfs_unmount(lfs: *LfsT) callconv(.c) i32; +extern fn lfs_file_open(lfs: *LfsT, file: *LfsFileT, path: [*:0]const u8, flags: i32) callconv(.c) i32; +extern fn lfs_file_close(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32; +extern fn lfs_file_read(lfs: *LfsT, file: *LfsFileT, buf: [*]u8, size: u32) callconv(.c) i32; +extern fn lfs_file_write(lfs: *LfsT, file: *LfsFileT, buf: [*]const u8, size: u32) callconv(.c) i32; +extern fn lfs_file_sync(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32; +extern fn lfs_file_seek(lfs: *LfsT, file: *LfsFileT, off: i32, whence: i32) callconv(.c) i32; +extern fn lfs_file_size(lfs: *LfsT, file: *LfsFileT) callconv(.c) i32; +extern fn lfs_remove(lfs: *LfsT, path: [*:0]const u8) callconv(.c) i32; +extern fn lfs_mkdir(lfs: *LfsT, path: [*:0]const u8) callconv(.c) i32; +extern fn lfs_stat(lfs: *LfsT, path: [*:0]const u8, info: *LfsInfo) callconv(.c) i32; + +// --- Static state --- +// LittleFS requires ~800 bytes for lfs_t. We over-allocate to be safe. +var lfs_state: [2048]u8 align(8) = [_]u8{0} ** 2048; +var lfs_mounted: bool = false; + +// Static buffers to avoid malloc for cache/lookahead +var read_cache: [CACHE_SIZE]u8 = [_]u8{0} ** CACHE_SIZE; +var prog_cache: [CACHE_SIZE]u8 = [_]u8{0} ** CACHE_SIZE; +var lookahead_buf: [LOOKAHEAD_SIZE]u8 = [_]u8{0} ** LOOKAHEAD_SIZE; + +// File handles: pre-allocated pool (LittleFS lfs_file_t is ~100 bytes, over-allocate) +const MAX_LFS_FILES = 8; +var file_slots: [MAX_LFS_FILES][512]u8 align(8) = [_][512]u8{[_]u8{0} ** 512} ** MAX_LFS_FILES; +var file_active: [MAX_LFS_FILES]bool = [_]bool{false} ** MAX_LFS_FILES; + +var cfg: LfsConfig = .{ + .context = null, + .read = &lfsRead, + .prog = &lfsProg, + .erase = &lfsErase, + .sync = &lfsSync, + .read_size = SECTOR_SIZE, + .prog_size = SECTOR_SIZE, + .block_size = BLOCK_SIZE, + .block_count = BLOCK_COUNT, + .block_cycles = 500, + .cache_size = CACHE_SIZE, + .lookahead_size = LOOKAHEAD_SIZE, + .compact_thresh = 0, + .read_buffer = &read_cache, + .prog_buffer = &prog_cache, + .lookahead_buffer = &lookahead_buf, + .name_max = 0, + .file_max = 0, + .attr_max = 0, + .metadata_max = 0, + .inline_max = 0, +}; + +// ========================================================= +// LittleFS Config Callbacks +// ========================================================= + +/// Read a region from a block via VirtIO-Block. +fn lfsRead(_: *LfsConfig, block: u32, off: u32, buffer: ?*anyopaque, size: u32) callconv(.c) i32 { + const buf: [*]u8 = @ptrCast(@alignCast(buffer orelse return -5)); + const base_sector: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, off) / SECTOR_SIZE; + const sector_offset = off % SECTOR_SIZE; + + if (sector_offset == 0 and size % SECTOR_SIZE == 0) { + // Aligned: direct sector reads + var i: u32 = 0; + while (i < size / SECTOR_SIZE) : (i += 1) { + virtio_blk_read(base_sector + i, buf + i * SECTOR_SIZE); + } + } else { + // Unaligned: bounce buffer + var tmp: [SECTOR_SIZE]u8 = undefined; + var remaining: u32 = size; + var buf_off: u32 = 0; + var cur_off: u32 = off; + + while (remaining > 0) { + const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, cur_off) / SECTOR_SIZE; + const sec_off = cur_off % SECTOR_SIZE; + virtio_blk_read(sec, &tmp); + + const avail = SECTOR_SIZE - sec_off; + const chunk = if (remaining < avail) remaining else avail; + for (0..chunk) |j| { + buf[buf_off + @as(u32, @intCast(j))] = tmp[sec_off + @as(u32, @intCast(j))]; + } + buf_off += chunk; + cur_off += chunk; + remaining -= chunk; + } + } + return 0; +} + +/// Program (write) a region in a block via VirtIO-Block. +fn lfsProg(_: *LfsConfig, block: u32, off: u32, buffer: ?*anyopaque, size: u32) callconv(.c) i32 { + const buf: [*]const u8 = @ptrCast(@alignCast(buffer orelse return -5)); + const base_sector: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, off) / SECTOR_SIZE; + const sector_offset = off % SECTOR_SIZE; + + if (sector_offset == 0 and size % SECTOR_SIZE == 0) { + // Aligned: direct sector writes + var i: u32 = 0; + while (i < size / SECTOR_SIZE) : (i += 1) { + virtio_blk_write(base_sector + i, buf + i * SECTOR_SIZE); + } + } else { + // Unaligned: read-modify-write via bounce buffer + var tmp: [SECTOR_SIZE]u8 = undefined; + var remaining: u32 = size; + var buf_off: u32 = 0; + var cur_off: u32 = off; + + while (remaining > 0) { + const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + @as(u64, cur_off) / SECTOR_SIZE; + const sec_off = cur_off % SECTOR_SIZE; + + // Read existing sector if partial write + if (sec_off != 0 or remaining < SECTOR_SIZE) { + virtio_blk_read(sec, &tmp); + } + + const avail = SECTOR_SIZE - sec_off; + const chunk = if (remaining < avail) remaining else avail; + for (0..chunk) |j| { + tmp[sec_off + @as(u32, @intCast(j))] = buf[buf_off + @as(u32, @intCast(j))]; + } + virtio_blk_write(sec, &tmp); + buf_off += chunk; + cur_off += chunk; + remaining -= chunk; + } + } + return 0; +} + +/// Erase a block. VirtIO-Block has no erase concept, so we zero-fill. +fn lfsErase(_: *LfsConfig, block: u32) callconv(.c) i32 { + const zeros = [_]u8{0xFF} ** SECTOR_SIZE; // LFS expects 0xFF after erase + var i: u32 = 0; + while (i < SECTORS_PER_BLOCK) : (i += 1) { + const sec: u64 = @as(u64, block) * SECTORS_PER_BLOCK + i; + virtio_blk_write(sec, &zeros); + } + return 0; +} + +/// Sync — VirtIO-Block is synchronous, nothing to flush. +fn lfsSync(_: *LfsConfig) callconv(.c) i32 { + return 0; +} + +// ========================================================= +// Exported C-ABI for Nim L1 +// ========================================================= + +/// Format the block device with LittleFS. +export fn nexus_lfs_format() i32 { + kprint("[LFS] Formatting sovereign filesystem...\n"); + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const rc = lfs_format(lfs_ptr, &cfg); + if (rc == 0) { + kprint("[LFS] Format OK\n"); + } else { + kprint("[LFS] Format FAILED\n"); + } + return rc; +} + +/// Mount the LittleFS filesystem. Auto-formats if mount fails (first boot). +export fn nexus_lfs_mount() i32 { + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + var rc = lfs_mount(lfs_ptr, &cfg); + if (rc != 0) { + // First boot or corrupt — format and retry + kprint("[LFS] Mount failed, formatting (first boot)...\n"); + rc = lfs_format(lfs_ptr, &cfg); + if (rc != 0) { + kprint("[LFS] Format FAILED\n"); + return rc; + } + rc = lfs_mount(lfs_ptr, &cfg); + } + if (rc == 0) { + lfs_mounted = true; + kprint("[LFS] Sovereign filesystem mounted on /nexus\n"); + } else { + kprint("[LFS] Mount FAILED after format\n"); + } + return rc; +} + +/// Unmount the filesystem. +export fn nexus_lfs_unmount() i32 { + if (!lfs_mounted) return -1; + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const rc = lfs_unmount(lfs_ptr); + lfs_mounted = false; + return rc; +} + +/// Open a file. Returns a file handle index (0..MAX_LFS_FILES-1) or -1 on error. +/// flags: 1=RDONLY, 2=WRONLY, 3=RDWR, 0x0100=CREAT, 0x0400=TRUNC, 0x0800=APPEND +export fn nexus_lfs_open(path: [*:0]const u8, flags: i32) i32 { + if (!lfs_mounted) return -1; + // Find free slot + var slot: usize = 0; + while (slot < MAX_LFS_FILES) : (slot += 1) { + if (!file_active[slot]) break; + } + if (slot >= MAX_LFS_FILES) return -1; // No free handles + + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[slot])); + const rc = lfs_file_open(lfs_ptr, file_ptr, path, flags); + if (rc == 0) { + file_active[slot] = true; + return @intCast(slot); + } + return rc; +} + +/// Read from a file. Returns bytes read or negative error. +export fn nexus_lfs_read(handle: i32, buf: [*]u8, size: u32) i32 { + if (!lfs_mounted) return -1; + const idx: usize = @intCast(handle); + if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1; + + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx])); + return lfs_file_read(lfs_ptr, file_ptr, buf, size); +} + +/// Write to a file. Returns bytes written or negative error. +export fn nexus_lfs_write(handle: i32, buf: [*]const u8, size: u32) i32 { + if (!lfs_mounted) return -1; + const idx: usize = @intCast(handle); + if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1; + + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx])); + return lfs_file_write(lfs_ptr, file_ptr, buf, size); +} + +/// Close a file handle. +export fn nexus_lfs_close(handle: i32) i32 { + if (!lfs_mounted) return -1; + const idx: usize = @intCast(handle); + if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1; + + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx])); + const rc = lfs_file_close(lfs_ptr, file_ptr); + file_active[idx] = false; + return rc; +} + +/// Seek within a file. +export fn nexus_lfs_seek(handle: i32, off: i32, whence: i32) i32 { + if (!lfs_mounted) return -1; + const idx: usize = @intCast(handle); + if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1; + + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx])); + return lfs_file_seek(lfs_ptr, file_ptr, off, whence); +} + +/// Get file size. +export fn nexus_lfs_size(handle: i32) i32 { + if (!lfs_mounted) return -1; + const idx: usize = @intCast(handle); + if (idx >= MAX_LFS_FILES or !file_active[idx]) return -1; + + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + const file_ptr: *LfsFileT = @ptrCast(@alignCast(&file_slots[idx])); + return lfs_file_size(lfs_ptr, file_ptr); +} + +/// Remove a file or empty directory. +export fn nexus_lfs_remove(path: [*:0]const u8) i32 { + if (!lfs_mounted) return -1; + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + return lfs_remove(lfs_ptr, path); +} + +/// Create a directory. +export fn nexus_lfs_mkdir(path: [*:0]const u8) i32 { + if (!lfs_mounted) return -1; + const lfs_ptr: *LfsT = @ptrCast(@alignCast(&lfs_state)); + return lfs_mkdir(lfs_ptr, path); +} + +/// Check if mounted. +export fn nexus_lfs_is_mounted() i32 { + return if (lfs_mounted) @as(i32, 1) else @as(i32, 0); +} diff --git a/hal/virtio_mmio.zig b/hal/virtio_mmio.zig new file mode 100644 index 0000000..f52cbc7 --- /dev/null +++ b/hal/virtio_mmio.zig @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Rumpk HAL: VirtIO MMIO Transport Layer (ARM64) +//! +//! Provides the same VirtioTransport API as virtio_pci.zig but for MMIO-based +//! VirtIO devices as found on QEMU aarch64 virt machine. +//! +//! QEMU virt MMIO layout: 32 slots starting at 0x0a000000, stride 0x200. +//! Each slot triggers GIC SPI (IRQ 48 + slot_index). +//! +//! Supports both legacy (v1) and modern (v2) MMIO transport. +//! +//! SAFETY: All hardware registers accessed via volatile pointers. + +const std = @import("std"); +const builtin = @import("builtin"); +const uart = @import("uart.zig"); + +// ========================================================= +// VirtIO MMIO Register Offsets (spec §4.2.2) +// ========================================================= + +const VIRTIO_MMIO_MAGIC_VALUE = 0x000; +const VIRTIO_MMIO_VERSION = 0x004; +const VIRTIO_MMIO_DEVICE_ID = 0x008; +const VIRTIO_MMIO_VENDOR_ID = 0x00C; +const VIRTIO_MMIO_DEVICE_FEATURES = 0x010; +const VIRTIO_MMIO_DEVICE_FEATURES_SEL = 0x014; +const VIRTIO_MMIO_DRIVER_FEATURES = 0x020; +const VIRTIO_MMIO_DRIVER_FEATURES_SEL = 0x024; +const VIRTIO_MMIO_QUEUE_SEL = 0x030; +const VIRTIO_MMIO_QUEUE_NUM_MAX = 0x034; +const VIRTIO_MMIO_QUEUE_NUM = 0x038; +const VIRTIO_MMIO_QUEUE_ALIGN = 0x03C; +const VIRTIO_MMIO_QUEUE_PFN = 0x040; +const VIRTIO_MMIO_QUEUE_READY = 0x044; +const VIRTIO_MMIO_QUEUE_NOTIFY = 0x050; +const VIRTIO_MMIO_INTERRUPT_STATUS = 0x060; +const VIRTIO_MMIO_INTERRUPT_ACK = 0x064; +const VIRTIO_MMIO_STATUS = 0x070; +const VIRTIO_MMIO_QUEUE_DESC_LOW = 0x080; +const VIRTIO_MMIO_QUEUE_DESC_HIGH = 0x084; +const VIRTIO_MMIO_QUEUE_AVAIL_LOW = 0x090; +const VIRTIO_MMIO_QUEUE_AVAIL_HIGH = 0x094; +const VIRTIO_MMIO_QUEUE_USED_LOW = 0x0A0; +const VIRTIO_MMIO_QUEUE_USED_HIGH = 0x0A4; +const VIRTIO_MMIO_CONFIG = 0x100; // Device-specific config starts here + +// VirtIO magic value: "virt" in little-endian +const VIRTIO_MAGIC: u32 = 0x74726976; + +// ========================================================= +// QEMU virt MMIO Topology +// ========================================================= + +const MMIO_BASE: usize = 0x0a000000; +const MMIO_STRIDE: usize = 0x200; +const MMIO_SLOT_COUNT: usize = 32; +const MMIO_IRQ_BASE: u32 = 48; // GIC SPI base for VirtIO MMIO + +// ========================================================= +// MMIO Read/Write Helpers +// ========================================================= + +fn mmio_read(base: usize, offset: usize) u32 { + const ptr: *volatile u32 = @ptrFromInt(base + offset); + return ptr.*; +} + +fn mmio_write(base: usize, offset: usize, val: u32) void { + const ptr: *volatile u32 = @ptrFromInt(base + offset); + ptr.* = val; +} + +fn mmio_read_u8(base: usize, offset: usize) u8 { + const ptr: *volatile u8 = @ptrFromInt(base + offset); + return ptr.*; +} + +// ========================================================= +// Arch-safe memory barrier +// ========================================================= + +pub inline fn io_barrier() void { + switch (builtin.cpu.arch) { + .aarch64 => asm volatile ("dmb sy" ::: .{ .memory = true }), + .riscv64 => asm volatile ("fence" ::: .{ .memory = true }), + else => @compileError("unsupported arch"), + } +} + +// ========================================================= +// VirtIO MMIO Transport (same API surface as PCI transport) +// ========================================================= + +pub const VirtioTransport = struct { + base_addr: usize, + is_modern: bool, + version: u32, + + // Legacy compatibility fields (match PCI transport shape) + legacy_bar: usize, + + // Modern interface placeholders (unused for MMIO but present for API compat) + common_cfg: ?*volatile anyopaque, + notify_cfg: ?usize, + notify_off_multiplier: u32, + isr_cfg: ?*volatile u8, + device_cfg: ?*volatile u8, + + pub fn init(mmio_base: usize) VirtioTransport { + return .{ + .base_addr = mmio_base, + .is_modern = false, + .version = 0, + .legacy_bar = 0, + .common_cfg = null, + .notify_cfg = null, + .notify_off_multiplier = 0, + .isr_cfg = null, + .device_cfg = null, + }; + } + + pub fn probe(self: *VirtioTransport) bool { + const magic = mmio_read(self.base_addr, VIRTIO_MMIO_MAGIC_VALUE); + if (magic != VIRTIO_MAGIC) return false; + + self.version = mmio_read(self.base_addr, VIRTIO_MMIO_VERSION); + if (self.version != 1 and self.version != 2) return false; + + const device_id = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_ID); + if (device_id == 0) return false; // No device at this slot + + self.is_modern = (self.version == 2); + + uart.print("[VirtIO-MMIO] Probed 0x"); + uart.print_hex(self.base_addr); + uart.print(" Ver="); + uart.print_hex(self.version); + uart.print(" DevID="); + uart.print_hex(device_id); + uart.print("\n"); + + return true; + } + + pub fn reset(self: *VirtioTransport) void { + self.set_status(0); + // After reset, wait for device to reinitialize (spec §2.1.1) + io_barrier(); + } + + pub fn get_status(self: *VirtioTransport) u8 { + return @truncate(mmio_read(self.base_addr, VIRTIO_MMIO_STATUS)); + } + + pub fn set_status(self: *VirtioTransport, status: u8) void { + mmio_write(self.base_addr, VIRTIO_MMIO_STATUS, @as(u32, status)); + } + + pub fn add_status(self: *VirtioTransport, status: u8) void { + self.set_status(self.get_status() | status); + } + + pub fn select_queue(self: *VirtioTransport, idx: u16) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_SEL, @as(u32, idx)); + } + + pub fn get_queue_size(self: *VirtioTransport) u16 { + return @truncate(mmio_read(self.base_addr, VIRTIO_MMIO_QUEUE_NUM_MAX)); + } + + pub fn set_queue_size(self: *VirtioTransport, size: u16) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NUM, @as(u32, size)); + } + + pub fn setup_legacy_queue(self: *VirtioTransport, pfn: u32) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_ALIGN, 4096); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_PFN, pfn); + } + + pub fn setup_modern_queue(self: *VirtioTransport, desc: u64, avail: u64, used: u64) void { + // Set queue size first + const max_size = mmio_read(self.base_addr, VIRTIO_MMIO_QUEUE_NUM_MAX); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NUM, max_size); + + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_DESC_LOW, @truncate(desc)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_DESC_HIGH, @truncate(desc >> 32)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_AVAIL_LOW, @truncate(avail)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, @truncate(avail >> 32)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_USED_LOW, @truncate(used)); + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_USED_HIGH, @truncate(used >> 32)); + + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_READY, 1); + } + + pub fn notify(self: *VirtioTransport, queue_idx: u16) void { + mmio_write(self.base_addr, VIRTIO_MMIO_QUEUE_NOTIFY, @as(u32, queue_idx)); + } + + // ========================================================= + // Unified Accessor API (matches PCI transport extensions) + // ========================================================= + + pub fn get_device_features(self: *VirtioTransport) u64 { + mmio_write(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 0); + io_barrier(); + const low: u64 = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES); + + mmio_write(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 1); + io_barrier(); + const high: u64 = mmio_read(self.base_addr, VIRTIO_MMIO_DEVICE_FEATURES); + + return (high << 32) | low; + } + + pub fn set_driver_features(self: *VirtioTransport, features: u64) void { + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0); + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES, @truncate(features)); + io_barrier(); + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1); + mmio_write(self.base_addr, VIRTIO_MMIO_DRIVER_FEATURES, @truncate(features >> 32)); + io_barrier(); + } + + pub fn get_device_config_byte(self: *VirtioTransport, offset: usize) u8 { + return mmio_read_u8(self.base_addr, VIRTIO_MMIO_CONFIG + offset); + } + + pub fn ack_interrupt(self: *VirtioTransport) u32 { + const status = mmio_read(self.base_addr, VIRTIO_MMIO_INTERRUPT_STATUS); + mmio_write(self.base_addr, VIRTIO_MMIO_INTERRUPT_ACK, status); + return status; + } +}; + +// ========================================================= +// Device Discovery +// ========================================================= + +/// Scan MMIO slots for a VirtIO device with the given device ID. +/// Returns MMIO base address or null if not found. +pub fn find_device(device_id: u32) ?usize { + var slot: usize = 0; + while (slot < MMIO_SLOT_COUNT) : (slot += 1) { + const base = MMIO_BASE + (slot * MMIO_STRIDE); + const magic = mmio_read(base, VIRTIO_MMIO_MAGIC_VALUE); + if (magic != VIRTIO_MAGIC) continue; + + const dev_id = mmio_read(base, VIRTIO_MMIO_DEVICE_ID); + if (dev_id == device_id) { + return base; + } + } + return null; +} + +/// Get the GIC SPI number for a given MMIO slot base address. +pub fn slot_irq(base: usize) u32 { + const slot = (base - MMIO_BASE) / MMIO_STRIDE; + return MMIO_IRQ_BASE + @as(u32, @intCast(slot)); +} diff --git a/libs/libertaria/lwf_adapter.zig b/libs/libertaria/lwf_adapter.zig new file mode 100644 index 0000000..146e56b --- /dev/null +++ b/libs/libertaria/lwf_adapter.zig @@ -0,0 +1,512 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Project LibWeb: LWF Adapter for Rumpk Kernel +//! +//! Freestanding LWF header parser for kernel-side routing decisions. +//! Zero-copy — operates directly on ION slab buffers. +//! Does NOT use std.mem.Allocator — all parsing is in-place. +//! +//! The full LWF codec (with allocation, encode, checksum) runs in +//! the Membrane (userland) where std is available. The kernel only +//! needs to parse the header to decide routing. +//! +//! Wire Format (after Ethernet header): +//! [Eth 14B][LWF Header 88B][Payload ...][LWF Trailer 36B] +//! +//! Integration Points: +//! - NetSwitch: EtherType 0x4C57 ("LW") → route to chan_lwf_rx +//! - Membrane: Full LWF codec via upstream lwf.zig (uses std) + +// ========================================================= +// LWF Constants (RFC-0000 v0.3.1) +// ========================================================= + +pub const ETHERTYPE_LWF: u16 = 0x4C57; // "LW" in ASCII — Sovereign EtherType +pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 }; +pub const LWF_VERSION: u8 = 0x02; +pub const HEADER_SIZE: usize = 88; +pub const TRAILER_SIZE: usize = 36; +pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE; // 124 bytes + +// ========================================================= +// Frame Classes (RFC-0000 Section 4.2) +// ========================================================= + +pub const FrameClass = enum(u8) { + micro = 0x00, // 128 bytes total + mini = 0x01, // 512 bytes total + standard = 0x02, // 1350 bytes total + big = 0x03, // 4096 bytes total (exceeds ION slab!) + jumbo = 0x04, // 9000 bytes total (exceeds ION slab!) + _, + + pub fn maxTotal(self: FrameClass) u16 { + return switch (self) { + .micro => 128, + .mini => 512, + .standard => 1350, + .big => 4096, + .jumbo => 9000, + _ => 0, + }; + } + + /// Check if frame class fits in an ION slab (2048 bytes) + pub fn fitsInSlab(self: FrameClass) bool { + return switch (self) { + .micro, .mini, .standard => true, + .big, .jumbo => false, + _ => false, + }; + } +}; + +// ========================================================= +// Service Types (RFC-0121) +// ========================================================= + +pub const ServiceType = struct { + pub const DATA_TRANSPORT: u16 = 0x0001; + pub const SLASH_PROTOCOL: u16 = 0x0002; + pub const IDENTITY_SIGNAL: u16 = 0x0003; + pub const ECONOMIC_SETTLEMENT: u16 = 0x0004; + pub const RELAY_FORWARD: u16 = 0x0005; + pub const STREAM_AUDIO: u16 = 0x0800; + pub const STREAM_VIDEO: u16 = 0x0801; + pub const STREAM_DATA: u16 = 0x0802; + pub const SWARM_MANIFEST: u16 = 0x0B00; + pub const SWARM_HAVE: u16 = 0x0B01; + pub const SWARM_REQUEST: u16 = 0x0B02; + pub const SWARM_BLOCK: u16 = 0x0B03; +}; + +// ========================================================= +// LWF Flags (RFC-0000 Section 4.3) +// ========================================================= + +pub const Flags = struct { + pub const ENCRYPTED: u8 = 0x01; + pub const SIGNED: u8 = 0x02; + pub const RELAYABLE: u8 = 0x04; + pub const HAS_ENTROPY: u8 = 0x08; + pub const FRAGMENTED: u8 = 0x10; + pub const PRIORITY: u8 = 0x20; +}; + +// ========================================================= +// LWF Header View (Zero-Copy over ION slab) +// ========================================================= + +/// Parsed header fields from a raw buffer. No allocation. +/// All multi-byte integers are stored big-endian on the wire. +pub const HeaderView = struct { + /// Pointer to the start of the LWF header in the ION slab + raw: [*]const u8, + + // Pre-parsed routing fields (hot path) + service_type: u16, + payload_len: u16, + frame_class: FrameClass, + version: u8, + flags: u8, + sequence: u32, + timestamp: u64, + + /// Parse header from raw bytes. Returns null if invalid. + /// Does NOT copy — references the original buffer. + pub fn parse(data: [*]const u8, data_len: u16) ?HeaderView { + if (data_len < HEADER_SIZE) return null; + + // Fast reject: Magic check (4 bytes at offset 0) + if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0) + return null; + + // Version check (offset 77) + const ver = data[77]; + if (ver != LWF_VERSION) return null; + + // Parse routing-critical fields + const service = readU16Big(data[72..74]); + const plen = readU16Big(data[74..76]); + const fclass = data[76]; + const flg = data[78]; + const seq = readU32Big(data[68..72]); + const ts = readU64Big(data[80..88]); + + // Sanity: payload_len must fit in remaining buffer + const total_needed = HEADER_SIZE + @as(usize, plen) + TRAILER_SIZE; + if (total_needed > data_len) return null; + + return HeaderView{ + .raw = data, + .service_type = service, + .payload_len = plen, + .frame_class = @enumFromInt(fclass), + .version = ver, + .flags = flg, + .sequence = seq, + .timestamp = ts, + }; + } + + /// Get destination hint (24 bytes at offset 4) + pub fn destHint(self: *const HeaderView) *const [24]u8 { + return @ptrCast(self.raw[4..28]); + } + + /// Get source hint (24 bytes at offset 28) + pub fn sourceHint(self: *const HeaderView) *const [24]u8 { + return @ptrCast(self.raw[28..52]); + } + + /// Get session ID (16 bytes at offset 52) + pub fn sessionId(self: *const HeaderView) *const [16]u8 { + return @ptrCast(self.raw[52..68]); + } + + /// Get pointer to payload data (starts at offset 88) + pub fn payloadPtr(self: *const HeaderView) [*]const u8 { + return self.raw + HEADER_SIZE; + } + + /// Check if frame has PRIORITY flag + pub fn isPriority(self: *const HeaderView) bool { + return (self.flags & Flags.PRIORITY) != 0; + } + + /// Check if frame is encrypted + pub fn isEncrypted(self: *const HeaderView) bool { + return (self.flags & Flags.ENCRYPTED) != 0; + } + + /// Total frame size (header + payload + trailer) + pub fn totalSize(self: *const HeaderView) usize { + return HEADER_SIZE + @as(usize, self.payload_len) + TRAILER_SIZE; + } +}; + +// ========================================================= +// Fast Path: Validation for NetSwitch +// ========================================================= + +/// Quick magic-byte check. Use before full parse for early rejection. +/// Expects data to point past the Ethernet header (14 bytes). +pub fn isLwfMagic(data: [*]const u8, len: u16) bool { + if (len < 4) return false; + return data[0] == 'L' and data[1] == 'W' and data[2] == 'F' and data[3] == 0; +} + +/// Validate and parse an LWF frame from an ION slab. +/// Returns the parsed header view, or null if invalid. +/// The ION slab data should start at the LWF header (after Ethernet strip). +pub fn validateFrame(data: [*]const u8, len: u16) ?HeaderView { + return HeaderView.parse(data, len); +} + +// ========================================================= +// C ABI Exports (for Nim FFI) +// ========================================================= + +/// Check if a raw buffer contains a valid LWF frame. +/// Called from netswitch.nim to decide routing. +/// Returns 1 if valid LWF, 0 otherwise. +export fn lwf_validate(data: [*]const u8, len: u16) u8 { + if (HeaderView.parse(data, len)) |_| { + return 1; + } + return 0; +} + +/// Get the service type from a validated LWF frame. +/// Returns 0 on invalid input. +export fn lwf_get_service_type(data: [*]const u8, len: u16) u16 { + if (HeaderView.parse(data, len)) |hdr| { + return hdr.service_type; + } + return 0; +} + +/// Get the payload length from a validated LWF frame. +export fn lwf_get_payload_len(data: [*]const u8, len: u16) u16 { + if (HeaderView.parse(data, len)) |hdr| { + return hdr.payload_len; + } + return 0; +} + +/// Check if frame has PRIORITY flag set. +export fn lwf_is_priority(data: [*]const u8, len: u16) u8 { + if (HeaderView.parse(data, len)) |hdr| { + return if (hdr.isPriority()) 1 else 0; + } + return 0; +} + +// ========================================================= +// Freestanding Integer Helpers (no std) +// ========================================================= + +inline fn readU16Big(bytes: *const [2]u8) u16 { + return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]); +} + +inline fn readU32Big(bytes: *const [4]u8) u32 { + return (@as(u32, bytes[0]) << 24) | + (@as(u32, bytes[1]) << 16) | + (@as(u32, bytes[2]) << 8) | + @as(u32, bytes[3]); +} + +inline fn readU64Big(bytes: *const [8]u8) u64 { + return (@as(u64, bytes[0]) << 56) | + (@as(u64, bytes[1]) << 48) | + (@as(u64, bytes[2]) << 40) | + (@as(u64, bytes[3]) << 32) | + (@as(u64, bytes[4]) << 24) | + (@as(u64, bytes[5]) << 16) | + (@as(u64, bytes[6]) << 8) | + @as(u64, bytes[7]); +} + +inline fn writeU16Big(buf: *[2]u8, val: u16) void { + buf[0] = @truncate(val >> 8); + buf[1] = @truncate(val); +} + +inline fn writeU32Big(buf: *[4]u8, val: u32) void { + buf[0] = @truncate(val >> 24); + buf[1] = @truncate(val >> 16); + buf[2] = @truncate(val >> 8); + buf[3] = @truncate(val); +} + +inline fn writeU64Big(buf: *[8]u8, val: u64) void { + buf[0] = @truncate(val >> 56); + buf[1] = @truncate(val >> 48); + buf[2] = @truncate(val >> 40); + buf[3] = @truncate(val >> 32); + buf[4] = @truncate(val >> 24); + buf[5] = @truncate(val >> 16); + buf[6] = @truncate(val >> 8); + buf[7] = @truncate(val); +} + +// ========================================================= +// Test Frame Builder (for unit tests only) +// ========================================================= + +/// Build a minimal valid LWF frame in a buffer for testing. +/// Returns the total frame size written. +fn buildTestFrame(buf: []u8, payload: []const u8, service: u16, flags: u8) usize { + const total = HEADER_SIZE + payload.len + TRAILER_SIZE; + if (buf.len < total) return 0; + + // Zero the buffer + for (buf[0..total]) |*b| b.* = 0; + + // Magic + buf[0] = 'L'; + buf[1] = 'W'; + buf[2] = 'F'; + buf[3] = 0; + + // dest_hint (4..28) — leave zeros + // source_hint (28..52) — leave zeros + // session_id (52..68) — leave zeros + + // Sequence (68..72) + writeU32Big(buf[68..72], 1); + + // Service type (72..74) + writeU16Big(buf[72..74], service); + + // Payload len (74..76) + writeU16Big(buf[74..76], @truncate(payload.len)); + + // Frame class (76) + buf[76] = @intFromEnum(FrameClass.standard); + + // Version (77) + buf[77] = LWF_VERSION; + + // Flags (78) + buf[78] = flags; + + // entropy_difficulty (79) — 0 + + // Timestamp (80..88) + writeU64Big(buf[80..88], 0xDEADBEEF); + + // Payload + for (payload, 0..) |byte, i| { + buf[HEADER_SIZE + i] = byte; + } + + // Trailer (zeros = no signature, no checksum) + + return total; +} + +// ========================================================= +// Tests +// ========================================================= + +const testing = @import("std").testing; + +test "valid LWF frame parses correctly" { + var buf: [512]u8 = undefined; + const payload = "Hello LWF"; + const sz = buildTestFrame(&buf, payload, ServiceType.DATA_TRANSPORT, 0); + try testing.expect(sz > 0); + + const hdr = HeaderView.parse(&buf, @truncate(sz)); + try testing.expect(hdr != null); + + const h = hdr.?; + try testing.expectEqual(ServiceType.DATA_TRANSPORT, h.service_type); + try testing.expectEqual(@as(u16, 9), h.payload_len); + try testing.expectEqual(LWF_VERSION, h.version); + try testing.expectEqual(@as(u8, 0), h.flags); + try testing.expectEqual(@as(u32, 1), h.sequence); + try testing.expectEqual(@as(u64, 0xDEADBEEF), h.timestamp); + try testing.expectEqual(FrameClass.standard, h.frame_class); +} + +test "invalid magic rejected" { + var buf: [512]u8 = undefined; + _ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0); + + // Corrupt magic + buf[0] = 'X'; + const hdr = HeaderView.parse(&buf, 160); + try testing.expect(hdr == null); +} + +test "wrong version rejected" { + var buf: [512]u8 = undefined; + _ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0); + + // Set wrong version + buf[77] = 0x01; + const hdr = HeaderView.parse(&buf, 160); + try testing.expect(hdr == null); +} + +test "buffer too small rejected" { + var buf: [512]u8 = undefined; + _ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0); + + // Pass length smaller than header + const hdr = HeaderView.parse(&buf, 80); + try testing.expect(hdr == null); +} + +test "payload overflow rejected" { + var buf: [512]u8 = undefined; + _ = buildTestFrame(&buf, "test", ServiceType.DATA_TRANSPORT, 0); + + // Claim huge payload that doesn't fit + writeU16Big(buf[74..76], 5000); + const hdr = HeaderView.parse(&buf, 160); + try testing.expect(hdr == null); +} + +test "priority flag detection" { + var buf: [512]u8 = undefined; + const sz = buildTestFrame(&buf, "urgent", ServiceType.SLASH_PROTOCOL, Flags.PRIORITY); + + const hdr = HeaderView.parse(&buf, @truncate(sz)).?; + try testing.expect(hdr.isPriority()); + try testing.expect(!hdr.isEncrypted()); +} + +test "encrypted flag detection" { + var buf: [512]u8 = undefined; + const sz = buildTestFrame(&buf, "secret", ServiceType.IDENTITY_SIGNAL, Flags.ENCRYPTED | Flags.SIGNED); + + const hdr = HeaderView.parse(&buf, @truncate(sz)).?; + try testing.expect(hdr.isEncrypted()); + try testing.expect(!hdr.isPriority()); +} + +test "isLwfMagic fast path" { + var buf: [8]u8 = .{ 'L', 'W', 'F', 0, 0, 0, 0, 0 }; + try testing.expect(isLwfMagic(&buf, 8)); + + buf[2] = 'X'; + try testing.expect(!isLwfMagic(&buf, 8)); + + // Too short + try testing.expect(!isLwfMagic(&buf, 3)); +} + +test "C ABI lwf_validate matches HeaderView.parse" { + var buf: [512]u8 = undefined; + const sz = buildTestFrame(&buf, "abi_test", ServiceType.DATA_TRANSPORT, 0); + + try testing.expectEqual(@as(u8, 1), lwf_validate(&buf, @truncate(sz))); + + // Corrupt magic + buf[0] = 0; + try testing.expectEqual(@as(u8, 0), lwf_validate(&buf, @truncate(sz))); +} + +test "C ABI lwf_get_service_type" { + var buf: [512]u8 = undefined; + const sz = buildTestFrame(&buf, "svc", ServiceType.ECONOMIC_SETTLEMENT, 0); + + try testing.expectEqual(ServiceType.ECONOMIC_SETTLEMENT, lwf_get_service_type(&buf, @truncate(sz))); +} + +test "frame class slab fit check" { + try testing.expect(FrameClass.micro.fitsInSlab()); + try testing.expect(FrameClass.mini.fitsInSlab()); + try testing.expect(FrameClass.standard.fitsInSlab()); + try testing.expect(!FrameClass.big.fitsInSlab()); + try testing.expect(!FrameClass.jumbo.fitsInSlab()); +} + +test "totalSize calculation" { + var buf: [512]u8 = undefined; + const payload = "12345"; + const sz = buildTestFrame(&buf, payload, ServiceType.DATA_TRANSPORT, 0); + + const hdr = HeaderView.parse(&buf, @truncate(sz)).?; + try testing.expectEqual(@as(usize, 88 + 5 + 36), hdr.totalSize()); +} + +test "dest and source hint accessors" { + var buf: [512]u8 = undefined; + _ = buildTestFrame(&buf, "hint", ServiceType.DATA_TRANSPORT, 0); + + // Write a known dest hint at offset 4 + buf[4] = 0xAA; + buf[27] = 0xBB; + // Write a known source hint at offset 28 + buf[28] = 0xCC; + buf[51] = 0xDD; + + const hdr = HeaderView.parse(&buf, 160).?; + try testing.expectEqual(@as(u8, 0xAA), hdr.destHint()[0]); + try testing.expectEqual(@as(u8, 0xBB), hdr.destHint()[23]); + try testing.expectEqual(@as(u8, 0xCC), hdr.sourceHint()[0]); + try testing.expectEqual(@as(u8, 0xDD), hdr.sourceHint()[23]); +} + +test "session ID accessor" { + var buf: [512]u8 = undefined; + _ = buildTestFrame(&buf, "sess", ServiceType.DATA_TRANSPORT, 0); + + // Write session ID at offset 52 + buf[52] = 0x42; + buf[67] = 0x99; + + const hdr = HeaderView.parse(&buf, 160).?; + try testing.expectEqual(@as(u8, 0x42), hdr.sessionId()[0]); + try testing.expectEqual(@as(u8, 0x99), hdr.sessionId()[15]); +} diff --git a/libs/libertaria/lwf_membrane.zig b/libs/libertaria/lwf_membrane.zig new file mode 100644 index 0000000..d6ce49b --- /dev/null +++ b/libs/libertaria/lwf_membrane.zig @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: LCL-1.0 +// Copyright (c) 2026 Markus Maiwald +// Stewardship: Self Sovereign Society Foundation +// +// This file is part of the Nexus Commonwealth. +// See legal/LICENSE_COMMONWEALTH.md for license terms. + +//! Project LibWeb: LWF Membrane Client +//! +//! Userland-side LWF frame handler. Runs in the Membrane where std is +//! available. Consumes validated LWF frames from the dedicated ION +//! channel (s_lwf_rx) and produces encrypted outbound frames (s_lwf_tx). +//! +//! This module bridges: +//! - ION ring (SysTable s_lwf_rx/s_lwf_tx) for zero-copy kernel IPC +//! - Upstream LWF codec (libertaria-stack lwf.zig) for full encode/decode +//! - Noise Protocol for transport encryption/decryption +//! +//! Architecture: +//! VirtIO-net → NetSwitch (validates header) → chan_lwf_rx → [this module] +//! [this module] → chan_lwf_tx → NetSwitch → VirtIO-net +//! +//! NOTE: This file is NOT compiled freestanding. It targets the Membrane +//! (userland) and has access to std.mem.Allocator. + +const std = @import("std"); + +// ========================================================= +// ION Slab Constants (must match ion/memory.nim) +// ========================================================= + +const SLAB_SIZE: usize = 2048; +const SYSTABLE_ADDR: usize = if (builtin.cpu.arch == .aarch64) 0x50000000 else 0x83000000; +const ETH_HEADER_SIZE: usize = 14; + +// ========================================================= +// LWF Header Constants (duplicated from lwf_adapter.zig +// for use with std — adapter is freestanding, this is not) +// ========================================================= + +pub const HEADER_SIZE: usize = 88; +pub const TRAILER_SIZE: usize = 36; +pub const MIN_FRAME_SIZE: usize = HEADER_SIZE + TRAILER_SIZE; +pub const LWF_MAGIC = [4]u8{ 'L', 'W', 'F', 0 }; +pub const LWF_VERSION: u8 = 0x02; + +pub const Flags = struct { + pub const ENCRYPTED: u8 = 0x01; + pub const SIGNED: u8 = 0x02; + pub const RELAYABLE: u8 = 0x04; + pub const HAS_ENTROPY: u8 = 0x08; + pub const FRAGMENTED: u8 = 0x10; + pub const PRIORITY: u8 = 0x20; +}; + +// ========================================================= +// Frame Processing Result +// ========================================================= + +pub const FrameError = error{ + TooSmall, + InvalidMagic, + InvalidVersion, + PayloadOverflow, + DecryptionFailed, + NoSession, + SlabTooSmall, +}; + +pub const ProcessedFrame = struct { + service_type: u16, + payload: []const u8, // Points into slab — valid until ion_free + session_id: [16]u8, + dest_hint: [24]u8, + source_hint: [24]u8, + sequence: u32, + flags: u8, + encrypted: bool, +}; + +// ========================================================= +// LWF Membrane Client +// ========================================================= + +pub const LwfClient = struct { + /// Callback type for incoming LWF frames + pub const FrameHandler = *const fn (frame: ProcessedFrame) void; + + on_frame: ?FrameHandler, + + pub fn init() LwfClient { + return .{ + .on_frame = null, + }; + } + + /// Register a callback for incoming LWF frames + pub fn setHandler(self: *LwfClient, handler: FrameHandler) void { + self.on_frame = handler; + } + + /// Parse an LWF frame from a raw ION slab buffer. + /// The buffer starts AFTER the Ethernet header (NetSwitch strips it + /// to EtherType, but the ION packet still contains the full Ethernet + /// frame — so caller must offset by 14 bytes). + pub fn parseFrame(data: [*]const u8, len: u16) FrameError!ProcessedFrame { + if (len < HEADER_SIZE) return error.TooSmall; + + // Magic check + if (data[0] != 'L' or data[1] != 'W' or data[2] != 'F' or data[3] != 0) + return error.InvalidMagic; + + // Version check (offset 77) + if (data[77] != LWF_VERSION) return error.InvalidVersion; + + // Parse fields + const payload_len = readU16Big(data[74..76]); + const total_needed = HEADER_SIZE + @as(usize, payload_len) + TRAILER_SIZE; + if (total_needed > len) return error.PayloadOverflow; + + var frame: ProcessedFrame = undefined; + frame.service_type = readU16Big(data[72..74]); + frame.sequence = readU32Big(data[68..72]); + frame.flags = data[78]; + frame.encrypted = (frame.flags & Flags.ENCRYPTED) != 0; + frame.payload = data[HEADER_SIZE .. HEADER_SIZE + payload_len]; + + @memcpy(&frame.dest_hint, data[4..28]); + @memcpy(&frame.source_hint, data[28..52]); + @memcpy(&frame.session_id, data[52..68]); + + return frame; + } + + /// Build an outbound LWF frame into a slab buffer. + /// Returns the total frame size written. + pub fn buildFrame( + buf: []u8, + service_type: u16, + payload: []const u8, + session_id: [16]u8, + dest_hint: [24]u8, + source_hint: [24]u8, + sequence: u32, + flags: u8, + ) FrameError!usize { + const total = HEADER_SIZE + payload.len + TRAILER_SIZE; + if (buf.len < total) return error.SlabTooSmall; + + // Zero header + trailer regions + @memset(buf[0..HEADER_SIZE], 0); + @memset(buf[HEADER_SIZE + payload.len ..][0..TRAILER_SIZE], 0); + + // Magic + buf[0] = 'L'; + buf[1] = 'W'; + buf[2] = 'F'; + buf[3] = 0; + + // Dest/Source hints + @memcpy(buf[4..28], &dest_hint); + @memcpy(buf[28..52], &source_hint); + + // Session ID + @memcpy(buf[52..68], &session_id); + + // Sequence + writeU32Big(buf[68..72], sequence); + + // Service type + payload len + writeU16Big(buf[72..74], service_type); + writeU16Big(buf[74..76], @truncate(payload.len)); + + // Frame class (auto-select based on total size) + buf[76] = if (total <= 128) 0x00 // micro + else if (total <= 512) 0x01 // mini + else if (total <= 1350) 0x02 // standard + else if (total <= 4096) 0x03 // big + else 0x04; // jumbo + + // Version + buf[77] = LWF_VERSION; + + // Flags + buf[78] = flags; + + // Payload + @memcpy(buf[HEADER_SIZE..][0..payload.len], payload); + + return total; + } +}; + +// ========================================================= +// Integer Helpers +// ========================================================= + +fn readU16Big(bytes: *const [2]u8) u16 { + return (@as(u16, bytes[0]) << 8) | @as(u16, bytes[1]); +} + +fn readU32Big(bytes: *const [4]u8) u32 { + return (@as(u32, bytes[0]) << 24) | + (@as(u32, bytes[1]) << 16) | + (@as(u32, bytes[2]) << 8) | + @as(u32, bytes[3]); +} + +fn writeU16Big(buf: *[2]u8, val: u16) void { + buf[0] = @truncate(val >> 8); + buf[1] = @truncate(val); +} + +fn writeU32Big(buf: *[4]u8, val: u32) void { + buf[0] = @truncate(val >> 24); + buf[1] = @truncate(val >> 16); + buf[2] = @truncate(val >> 8); + buf[3] = @truncate(val); +} + +// ========================================================= +// Tests +// ========================================================= + +test "parseFrame valid" { + var buf: [512]u8 = undefined; + const payload = "Hello Noise"; + const sz = try LwfClient.buildFrame( + &buf, + 0x0001, // DATA_TRANSPORT + payload, + [_]u8{0xAA} ** 16, // session + [_]u8{0xBB} ** 24, // dest + [_]u8{0xCC} ** 24, // src + 42, + 0, + ); + + const frame = try LwfClient.parseFrame(&buf, @truncate(sz)); + try std.testing.expectEqual(@as(u16, 0x0001), frame.service_type); + try std.testing.expectEqual(@as(u32, 42), frame.sequence); + try std.testing.expectEqualSlices(u8, payload, frame.payload); + try std.testing.expectEqual(@as(u8, 0xAA), frame.session_id[0]); + try std.testing.expect(!frame.encrypted); +} + +test "parseFrame encrypted flag" { + var buf: [512]u8 = undefined; + const sz = try LwfClient.buildFrame( + &buf, + 0x0003, // IDENTITY_SIGNAL + "encrypted_payload", + [_]u8{0} ** 16, + [_]u8{0} ** 24, + [_]u8{0} ** 24, + 1, + Flags.ENCRYPTED | Flags.SIGNED, + ); + + const frame = try LwfClient.parseFrame(&buf, @truncate(sz)); + try std.testing.expect(frame.encrypted); + try std.testing.expectEqual(@as(u16, 0x0003), frame.service_type); +} + +test "buildFrame auto frame class" { + var buf: [2048]u8 = undefined; + + // Micro (total <= 128) + _ = try LwfClient.buildFrame(&buf, 0, "", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0); + try std.testing.expectEqual(@as(u8, 0x00), buf[76]); // micro + + // Standard (total > 512) + var payload: [500]u8 = undefined; + @memset(&payload, 0x42); + _ = try LwfClient.buildFrame(&buf, 0, &payload, [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0); + try std.testing.expectEqual(@as(u8, 0x02), buf[76]); // standard +} + +test "parseFrame rejects bad magic" { + var buf: [512]u8 = undefined; + _ = try LwfClient.buildFrame(&buf, 0, "x", [_]u8{0} ** 16, [_]u8{0} ** 24, [_]u8{0} ** 24, 0, 0); + buf[0] = 'X'; + try std.testing.expectError(error.InvalidMagic, LwfClient.parseFrame(&buf, 160)); +} + +test "buildFrame roundtrip preserves hints" { + var buf: [512]u8 = undefined; + const dest = [_]u8{0xDE} ** 24; + const src = [_]u8{0x5A} ** 24; + const sess = [_]u8{0xF0} ** 16; + + _ = try LwfClient.buildFrame(&buf, 0x0800, "audio", sess, dest, src, 99, Flags.PRIORITY); + + const frame = try LwfClient.parseFrame(&buf, 200); + try std.testing.expectEqual(@as(u16, 0x0800), frame.service_type); + try std.testing.expectEqual(@as(u32, 99), frame.sequence); + try std.testing.expectEqualSlices(u8, &dest, &frame.dest_hint); + try std.testing.expectEqualSlices(u8, &src, &frame.source_hint); + try std.testing.expectEqualSlices(u8, &sess, &frame.session_id); +} diff --git a/libs/membrane/include/math.h b/libs/membrane/include/math.h new file mode 100644 index 0000000..71d739c --- /dev/null +++ b/libs/membrane/include/math.h @@ -0,0 +1,16 @@ +/* Minimal math.h stub for freestanding Nim builds */ +#ifndef _MATH_H_STUB +#define _MATH_H_STUB + +static inline double fabs(double x) { return x < 0 ? -x : x; } +static inline float fabsf(float x) { return x < 0 ? -x : x; } +static inline double fmod(double x, double y) { return x - (long long)(x / y) * y; } +static inline double floor(double x) { return (double)(long long)x - (x < (double)(long long)x); } +static inline double ceil(double x) { return (double)(long long)x + (x > (double)(long long)x); } +static inline double round(double x) { return floor(x + 0.5); } + +#define HUGE_VAL __builtin_huge_val() +#define NAN __builtin_nan("") +#define INFINITY __builtin_inf() + +#endif diff --git a/libs/membrane/include/stdio.h b/libs/membrane/include/stdio.h new file mode 100644 index 0000000..b4e3c57 --- /dev/null +++ b/libs/membrane/include/stdio.h @@ -0,0 +1,13 @@ +#ifndef STDIO_H +#define STDIO_H +#include +#include +typedef void FILE; +#define stderr ((FILE*)0) +#define stdout ((FILE*)1) +int printf(const char* format, ...); +int sprintf(char* str, const char* format, ...); +int snprintf(char* str, size_t size, const char* format, ...); +size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream); +int fflush(FILE* stream); +#endif diff --git a/libs/membrane/include/stdlib.h b/libs/membrane/include/stdlib.h new file mode 100644 index 0000000..4c4e00e --- /dev/null +++ b/libs/membrane/include/stdlib.h @@ -0,0 +1,10 @@ +#ifndef STDLIB_H +#define STDLIB_H +#include +void* malloc(size_t size); +void free(void* ptr); +void* realloc(void* ptr, size_t size); +void abort(void); +void exit(int status); +int atoi(const char* str); +#endif diff --git a/run_aarch64.sh b/run_aarch64.sh new file mode 100755 index 0000000..ab670b7 --- /dev/null +++ b/run_aarch64.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# ============================================================================ +# Rumpk ARM64 QEMU Runner +# ============================================================================ +# Usage: +# ./run_aarch64.sh # Interactive boot +# ./run_aarch64.sh test # Automated boot test +# ============================================================================ +set -euo pipefail +cd "$(dirname "$0")" + +KERNEL="zig-out/bin/rumpk.elf" +DISK="build/disk_aarch64.img" + +if [ ! -f "$KERNEL" ]; then + echo "ERROR: $KERNEL not found. Run: zig build -Darch=aarch64" + exit 1 +fi + +# Create disk image if missing (16MB for LittleFS) +if [ ! -f "$DISK" ]; then + mkdir -p build + dd if=/dev/zero of="$DISK" bs=1M count=16 2>/dev/null + echo "Created $DISK (16MB)" +fi + +MODE="${1:-interactive}" + +# VirtIO MMIO devices for aarch64 virt +VIRTIO_NET="-device virtio-net-device,netdev=net0 -netdev user,id=net0" +VIRTIO_BLK="-device virtio-blk-device,drive=hd0 -drive if=none,id=hd0,format=raw,file=$DISK" + +if [ "$MODE" = "test" ]; then + echo "=== Rumpk ARM64 Boot Test ===" + TIMEOUT=20 + + LOGFILE=$(mktemp /tmp/rumpk-aarch64-XXXXXX.log) + timeout "$TIMEOUT" qemu-system-aarch64 \ + -M virt -cpu cortex-a72 -m 512M \ + -nographic -serial mon:stdio \ + -kernel "$KERNEL" \ + $VIRTIO_NET $VIRTIO_BLK \ + -no-reboot >"$LOGFILE" 2>&1 || true + + cat "$LOGFILE" + echo "" + echo "=== Boot Checks ===" + + PASS=0 + FAIL=0 + + check() { + local label="$1" + local pattern="$2" + if grep -qa "$pattern" "$LOGFILE"; then + echo " [PASS] $label" + PASS=$((PASS + 1)) + else + echo " [FAIL] $label" + FAIL=$((FAIL + 1)) + fi + } + + check "UART loopback" "Loopback Test: PASS" + check "aarch64_init reached" "aarch64_init reached" + check "GICv2 initialized" "GICv2 initialized" + check "Timer initialized" "Timer initialized" + check "Vectors installed" "vectors installed" + check "Nim handoff" "Handing off to Nim" + check "VirtIO-Net found" "VirtIO-Net" + check "VirtIO-Blk found" "VirtIO-Block\|VirtIO-Blk\|Storage initialized" + check "VirtIO init complete" "Initialization Complete" + + TOTAL=$((PASS + FAIL)) + echo "" + echo "=== Result: $PASS/$TOTAL ===" + if [ "$FAIL" -gt 0 ]; then + exit 1 + fi +else + exec qemu-system-aarch64 \ + -M virt -cpu cortex-a72 -m 512M \ + -nographic -serial mon:stdio \ + -kernel "$KERNEL" \ + $VIRTIO_NET $VIRTIO_BLK +fi diff --git a/vendor/littlefs/lfs_rumpk.h b/vendor/littlefs/lfs_rumpk.h new file mode 100644 index 0000000..60340da --- /dev/null +++ b/vendor/littlefs/lfs_rumpk.h @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BSD-3-Clause +// LittleFS freestanding configuration for Rumpk unikernel. +// +// Replaces lfs_util.h entirely via -DLFS_CONFIG=lfs_rumpk.h. +// Provides minimal stubs using kernel-provided malloc/free/memcpy/memset. +#ifndef LFS_RUMPK_H +#define LFS_RUMPK_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// --- NULL (freestanding stddef.h may not always provide it) --- +#ifndef NULL +#define NULL ((void *)0) +#endif + +// --- Logging: all disabled in freestanding kernel --- +#define LFS_TRACE(...) +#define LFS_DEBUG(...) +#define LFS_WARN(...) +#define LFS_ERROR(...) + +// --- Assertions: disabled --- +#define LFS_ASSERT(test) ((void)(test)) + +// --- Memory functions (provided by clib.c / libc_shim.zig) --- +extern void *malloc(unsigned long size); +extern void free(void *ptr); +extern void *memcpy(void *dest, const void *src, unsigned long n); +extern void *memset(void *s, int c, unsigned long n); +extern int memcmp(const void *s1, const void *s2, unsigned long n); +extern unsigned long strlen(const char *s); + +// strchr — needed by lfs.c for path parsing +static inline char *strchr(const char *s, int c) { + while (*s) { + if (*s == (char)c) return (char *)s; + s++; + } + return (c == '\0') ? (char *)s : NULL; +} + +static inline void *lfs_malloc(unsigned long size) { + return malloc(size); +} + +static inline void lfs_free(void *p) { + free(p); +} + +// --- Builtins --- +static inline uint32_t lfs_max(uint32_t a, uint32_t b) { + return (a > b) ? a : b; +} + +static inline uint32_t lfs_min(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) { + return a - (a % alignment); +} + +static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) { + return lfs_aligndown(a + alignment - 1, alignment); +} + +static inline uint32_t lfs_npw2(uint32_t a) { +#if defined(__GNUC__) || defined(__clang__) + return 32 - __builtin_clz(a - 1); +#else + uint32_t r = 0, s; + a -= 1; + s = (a > 0xffff) << 4; a >>= s; r |= s; + s = (a > 0xff ) << 3; a >>= s; r |= s; + s = (a > 0xf ) << 2; a >>= s; r |= s; + s = (a > 0x3 ) << 1; a >>= s; r |= s; + return (r | (a >> 1)) + 1; +#endif +} + +static inline uint32_t lfs_ctz(uint32_t a) { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_ctz(a); +#else + return lfs_npw2((a & -a) + 1) - 1; +#endif +} + +static inline uint32_t lfs_popc(uint32_t a) { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_popcount(a); +#else + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +#endif +} + +static inline int lfs_scmp(uint32_t a, uint32_t b) { + return (int)(unsigned)(a - b); +} + +// --- Endianness (RISC-V / ARM64 / x86_64 are all little-endian) --- +static inline uint32_t lfs_fromle32(uint32_t a) { return a; } +static inline uint32_t lfs_tole32(uint32_t a) { return a; } + +static inline uint32_t lfs_frombe32(uint32_t a) { + return __builtin_bswap32(a); +} +static inline uint32_t lfs_tobe32(uint32_t a) { + return __builtin_bswap32(a); +} + +// --- CRC-32 (implementation in lfs.c) --- +uint32_t lfs_crc(uint32_t crc, const void *buffer, unsigned long size); + +#ifdef __cplusplus +} +#endif + +#endif // LFS_RUMPK_H diff --git a/zig-cc-wrapper.sh b/zig-cc-wrapper.sh new file mode 100755 index 0000000..a3060e6 --- /dev/null +++ b/zig-cc-wrapper.sh @@ -0,0 +1,2 @@ +#!/bin/sh +zig cc -target riscv64-freestanding-none -mcpu=sifive_u54 "$@"