feat(rumpk): Phase 4 - NPL Loader

THE PLATFORM HAS PURPOSE
========================

Rumpk now has a payload loading system: NPL (Nexus Payload).

OUTPUT
------
[NPL]  Verification PASSED
[NPL] Executing payload...
[NPL]  Payload returned!
[NPL]  Bad sig rejected

NPL FORMAT (128-byte header)
----------------------------
- Magic: \x7fNPL (4 bytes)
- Version: 1 (1 byte)
- Arch: 0xAA=ARM64, 0xEE=x86_64, 0x55=RISC-V (1 byte)
- Flags: 2 bytes
- Signature: 64 bytes (Ed25519 placeholder)
- Body Size: 8 bytes
- Reserved: 48 bytes

IMPLEMENTATION
--------------
core/npl.nim:
- NPLHeader struct (packed, 128 bytes)
- loadNpl() - validates magic, version, arch, signature
- buildTestPayload() - creates test NPL in memory
- Signature verification (mock: rejects 0xFF)

TESTS VERIFIED
--------------
1. Valid payload: Loads and executes RET instruction
2. Bad signature: Correctly rejected (0xFF in sig[0])
3. Cross-arch: Would reject wrong arch code

PHASE SUMMARY
-------------
 Phase 1: Documentation (SPEC-008/009/010)
 Phase 2: Pure Zig libc (Freestanding Doctrine)
 Phase 3: Cooperative Fibers (Ping Pong)
 Phase 4: NPL Loader (with mock signature)
→ Phase 4.2: Ed25519 verification (Monocypher)
→ Phase 5: VisionFive 2 RISC-V hardware

The unikernel can now load and execute signed payloads.
Next: Real Ed25519 verification.
This commit is contained in:
Markus Maiwald 2025-12-30 08:21:45 +01:00
parent 4cc268683d
commit ee594df8a7
4 changed files with 222 additions and 13 deletions

View File

@ -0,0 +1,8 @@
0
5537 9466034 1767079280889767999 0d029e05059c175b5ce754f57f213b48 0 /home/markus/zWork/_Git/Nexus/core/rumpk/build/nimcache/@mnpl.nim.c
19164 69191110 1749873121000000000 fe5756ed84745fc96fd9dfb15050f599 0 /usr/lib/nim/nimbase.h
639 9459383 1767076381899751290 1b9448bcfa47e3161459266750e8ded4 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/limits.h
268 9459347 1767076422997272233 06a4c7da1c4987981a369ef3e003bda3 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stddef.h
155 9459777 1767076495338437553 9cc523d7a8a3a0bbc7c7af0fabeafc0b 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stdbool.h
924 9459799 1767076530759032485 73bc6834aef9958f6652470b63d7814b 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stdint.h
499 9459330 1767076360432003062 357ccd6329b0128cce0610c1443c600d 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/string.h

View File

@ -1,5 +1,5 @@
0 0
15874 9465108 1767078402329925246 e5c92c416746964294e1e5d350d1d6c3 0 /home/markus/zWork/_Git/Nexus/core/rumpk/build/nimcache/@mkernel.nim.c 23680 9466035 1767079280889767999 54c7101f99bb89eb7e9878f7b072ebef 0 /home/markus/zWork/_Git/Nexus/core/rumpk/build/nimcache/@mkernel.nim.c
19164 69191110 1749873121000000000 fe5756ed84745fc96fd9dfb15050f599 0 /usr/lib/nim/nimbase.h 19164 69191110 1749873121000000000 fe5756ed84745fc96fd9dfb15050f599 0 /usr/lib/nim/nimbase.h
639 9459383 1767076381899751290 1b9448bcfa47e3161459266750e8ded4 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/limits.h 639 9459383 1767076381899751290 1b9448bcfa47e3161459266750e8ded4 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/limits.h
268 9459347 1767076422997272233 06a4c7da1c4987981a369ef3e003bda3 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stddef.h 268 9459347 1767076422997272233 06a4c7da1c4987981a369ef3e003bda3 0 /home/markus/zWork/_Git/Nexus/core/rumpk/core/include/stddef.h

View File

@ -7,6 +7,7 @@
{.push stackTrace: off, lineTrace: off.} {.push stackTrace: off, lineTrace: off.}
import fiber import fiber
import npl
# ========================================================= # =========================================================
# HAL Imports from Zig (Layer 0) # HAL Imports from Zig (Layer 0)
@ -43,16 +44,6 @@ proc nimPanic(msg: cstring) {.exportc: "panic", cdecl, noreturn.} =
kprint("\n") kprint("\n")
rumpk_halt() rumpk_halt()
# =========================================================
# Memory Allocator - Provided by Zig L0 (hal/stubs.zig)
# =========================================================
# Zig exports: malloc, free, realloc, calloc
# We just import them for any explicit usage
proc malloc(size: csize_t): pointer {.importc, cdecl.}
proc free(p: pointer) {.importc, cdecl.}
proc realloc(p: pointer, size: csize_t): pointer {.importc, cdecl.}
# ========================================================= # =========================================================
# Fiber Test: Ping Pong # Fiber Test: Ping Pong
# ========================================================= # =========================================================
@ -81,6 +72,77 @@ proc thread_b_entry() {.cdecl.} =
while true: while true:
{.emit: "asm volatile(\"wfi\");".} {.emit: "asm volatile(\"wfi\");".}
# =========================================================
# NPL Loader Test
# =========================================================
var npl_test_buffer {.align: 128.}: array[256, uint8]
proc test_npl_loader() =
kprintln("")
kprintln("╔═══════════════════════════════════════╗")
kprintln("║ NPL Loader Test (Phase 4) ║")
kprintln("╚═══════════════════════════════════════╝")
kprintln("")
# Build test payload with architecture-specific RET instruction
when defined(arm64) or defined(aarch64):
let retCode = [0xC0'u8, 0x03'u8, 0x5F'u8, 0xD6'u8]
let archCode = ARCH_ARM64
kprintln("[NPL] Building ARM64 test payload...")
elif defined(amd64) or defined(x86_64):
let retCode = [0xC3'u8]
let archCode = ARCH_X86_64
kprintln("[NPL] Building x86_64 test payload...")
elif defined(riscv64):
let retCode = [0x67'u8, 0x80'u8, 0x00'u8, 0x00'u8]
let archCode = ARCH_RISCV64
kprintln("[NPL] Building RISC-V test payload...")
else:
kprintln("[NPL] Unsupported architecture!")
return
buildTestPayload(addr npl_test_buffer, archCode, retCode)
kprintln("[NPL] Loading...")
let entry = loadNpl(addr npl_test_buffer[0], 256)
kprintln("[NPL] loadNpl returned")
if entry != nil:
kprintln("[NPL] ✅ Verification PASSED")
kprintln("[NPL] Executing payload...")
entry()
kprintln("[NPL] ✅ Payload returned!")
else:
kprintln("[NPL] ❌ Load failed")
proc test_npl_rejection() =
kprintln("")
kprintln("[NPL] Testing rejection...")
when defined(arm64) or defined(aarch64):
let retCode = [0xC0'u8, 0x03'u8, 0x5F'u8, 0xD6'u8]
let archCode = ARCH_ARM64
elif defined(amd64) or defined(x86_64):
let retCode = [0xC3'u8]
let archCode = ARCH_X86_64
elif defined(riscv64):
let retCode = [0x67'u8, 0x80'u8, 0x00'u8, 0x00'u8]
let archCode = ARCH_RISCV64
else:
return
buildTestPayload(addr npl_test_buffer, archCode, retCode)
npl_test_buffer[8] = 0xFF'u8 # Trigger rejection
let entry = loadNpl(addr npl_test_buffer[0], 256)
if entry == nil:
kprintln("[NPL] ✅ Bad sig rejected")
else:
kprintln("[NPL] ❌ Should reject!")
# ========================================================= # =========================================================
# Kernel Main Entry # Kernel Main Entry
# ========================================================= # =========================================================
@ -96,6 +158,11 @@ proc kmain() {.exportc, cdecl.} =
kprintln("") kprintln("")
kprintln("[Rumpk L1] The Rubicon is crossed.") kprintln("[Rumpk L1] The Rubicon is crossed.")
kprintln("[Rumpk L1] Zig + Nim = Sovereign Metal.") kprintln("[Rumpk L1] Zig + Nim = Sovereign Metal.")
# Phase 4: NPL Loader Test
test_npl_loader()
test_npl_rejection()
kprintln("") kprintln("")
kprintln("[Rumpk L1] Initializing Fibers...") kprintln("[Rumpk L1] Initializing Fibers...")
@ -106,12 +173,10 @@ proc kmain() {.exportc, cdecl.} =
init_fiber(addr fiber_a, thread_a_entry, addr stack_a[0]) init_fiber(addr fiber_a, thread_a_entry, addr stack_a[0])
init_fiber(addr fiber_b, thread_b_entry, addr stack_b[0]) init_fiber(addr fiber_b, thread_b_entry, addr stack_b[0])
# Jump into the matrix
kprintln("[Kernel] Switching to Fiber A...") kprintln("[Kernel] Switching to Fiber A...")
kprintln("") kprintln("")
switch(addr fiber_a) switch(addr fiber_a)
# We should never get here unless Fiber A switches back to Main
nimPanic("Main thread returned!") nimPanic("Main thread returned!")
{.pop.} {.pop.}

136
core/npl.nim Normal file
View File

@ -0,0 +1,136 @@
# MARKUS MAIWALD (ARCHITECT) | VOXIS FORGE (AI)
# RUMPK CORE // NPL FORMAT
# The Contract of Execution.
{.push stackTrace: off, lineTrace: off.}
# =========================================================
# Constants
# =========================================================
const NPL_MAGIC*: array[4, uint8] = [0x7F'u8, 0x4E'u8, 0x50'u8, 0x4C'u8]
const NPL_VERSION* = 1'u8
const ARCH_ARM64* = 0xAA'u8
const ARCH_X86_64* = 0xEE'u8
const ARCH_RISCV64* = 0x55'u8
const NPL_HEADER_SIZE* = 128
# =========================================================
# Types
# =========================================================
type
NPLHeader* {.packed.} = object
magic*: array[4, uint8]
version*: uint8
arch*: uint8
flags*: uint16
signature*: array[64, uint8]
body_size*: uint64
reserved*: array[48, uint8]
PayloadEntry* = proc() {.cdecl.}
# Global last error (avoids struct return issues)
var nplLastError*: cstring = nil
# =========================================================
# Architecture Detection
# =========================================================
proc currentArch*(): uint8 =
when defined(arm64) or defined(aarch64):
return ARCH_ARM64
elif defined(amd64) or defined(x86_64):
return ARCH_X86_64
elif defined(riscv64):
return ARCH_RISCV64
else:
return 0x00'u8
# =========================================================
# Debug output
# =========================================================
proc console_write(p: pointer, len: csize_t) {.importc, cdecl.}
proc nplPrint(s: cstring) =
var i = 0
while s[i] != '\0':
inc i
console_write(cast[pointer](s), csize_t(i))
# =========================================================
# NPL Loader (returns nil on failure, sets nplLastError)
# =========================================================
proc loadNpl*(rawPtr: pointer, maxLen: uint64): PayloadEntry =
nplLastError = nil
nplPrint("[NPL] loadNpl\n")
if rawPtr == nil:
nplLastError = "null pointer"
return nil
let header = cast[ptr NPLHeader](rawPtr)
# Check Magic
if header.magic[0] != NPL_MAGIC[0] or
header.magic[1] != NPL_MAGIC[1] or
header.magic[2] != NPL_MAGIC[2] or
header.magic[3] != NPL_MAGIC[3]:
nplLastError = "bad magic"
return nil
# Check Version
if header.version != NPL_VERSION:
nplLastError = "bad version"
return nil
# Check Architecture
if header.arch != currentArch():
nplLastError = "bad arch"
return nil
# Check Bounds
let totalSize = NPL_HEADER_SIZE.uint64 + header.body_size
if totalSize > maxLen:
nplLastError = "truncated"
return nil
# Get body pointer
let bodyPtr = cast[pointer](cast[uint64](rawPtr) + NPL_HEADER_SIZE.uint64)
# Verify Signature (dev mode: reject 0xFF)
if header.signature[0] == 0xFF'u8:
nplLastError = "sig fail"
return nil
nplPrint("[NPL] valid\n")
# Cast body to function pointer
return cast[PayloadEntry](bodyPtr)
# =========================================================
# Helper: Build Test Payload
# =========================================================
proc buildTestPayload*(buffer: ptr array[256, uint8], arch: uint8,
codeBytes: openArray[uint8]) =
# Clear buffer
for i in 0..<256:
buffer[i] = 0
let header = cast[ptr NPLHeader](buffer)
header.magic = NPL_MAGIC
header.version = NPL_VERSION
header.arch = arch
header.flags = 1
header.body_size = codeBytes.len.uint64
# Copy code to body
for i, b in codeBytes:
buffer[NPL_HEADER_SIZE + i] = b
{.pop.}