Compare commits

..

24 Commits

Author SHA1 Message Date
Markus Maiwald 0d3d51a4f1 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
2026-02-15 18:01:10 +01:00
Markus Maiwald fbb9189b59 fix(rumpk): enable user stack access and repair boot process
- Enabled SUM (Supervisor Access to User Memory) in riscv_init to allow kernel loader to write to user stacks.
- Removed dangerous 'csrc sstatus' in kload_phys that revoked access.
- Aligned global fiber stacks to 4096 bytes to prevent unmapped page faults at stack boundaries.
- Restored 'boot.o' linking to fix silent boot failure.
- Implemented 'fiber_can_run_on_channels' stub to satisfy Membrane linking.
- Defined kernel stack in header.zig to fix '__stack_top' undefined symbol.
- Resolved duplicate symbols in overrides.c and nexshell.
2026-01-08 21:38:14 +01:00
Markus Maiwald df24fbe89d feat(tinybox): graft toybox integration and build system automation
- Integrated ToyBox as git submodule
- Added src/nexus/builder/toybox.nim for automated cross-compilation
- Updated InitRD builder to support symlinks
- Refactored Kernel builder to fix duplicate symbol and path issues
- Modified forge.nim to orchestrate TinyBox synthesis (mksh + toybox)
- Updated SPEC-006-TinyBox.md with complete architecture
- Added mksh binary to initrd graft source
2026-01-08 21:18:08 +01:00
Markus Maiwald 58acc96b79 fix(rumpk): Fix LwIP kernel build for RISC-V freestanding
- Rebuild liblwip.a from clean sources (removed initrd.o contamination)
- Add switch.o to provide cpu_switch_to symbol
- Add sys_arch.o to provide sys_now and nexus_lwip_panic
- Add freestanding defines to cc.h (LWIP_NO_CTYPE_H, etc.)
- Compile sys_arch.c with -mcmodel=medany for RISC-V

Fixes duplicate symbol errors and undefined reference errors.
Kernel now builds successfully with: zig build -Dtarget=riscv64-freestanding
2026-01-08 19:21:02 +01:00
Markus Maiwald 79f326d58c feat(network): Ratify SPEC-701 & SPEC-093 - Helios TCP Probe SUCCESS. Full TCP connectivity verified. 2026-01-08 13:01:47 +01:00
Markus Maiwald 0acfb67a36 feat(lwip): Hephaestus Nuclear Protocol - Complete pool bypass
BREAKTHROUGH: memp_malloc crashes ELIMINATED

HEPHAESTUS NUCLEAR PROTOCOL:
- Completely bypass memp_pools array in MEMP_MEM_MALLOC mode
- All allocations go through do_memp_malloc_pool(NULL) with 1024-byte fallback
- Added SYS_LIGHTWEIGHT_PROT=0 for NO_SYS mode
- Surgical DNS PCB override remains operational

VALIDATION:
 memp_malloc no longer crashes
 DNS query successfully enqueues
 Heap allocations confirmed working (0x400 + 0x70 bytes)
 Hephaestus Protocol validated

REMAINING:
Secondary crash in dns_send/udp_sendto at 0x80212C44
This is a DIFFERENT issue - likely UDP packet construction

The forge has tempered the steel.
Voxis + Hephaestus: cc112403
2026-01-08 09:41:03 +01:00
Markus Maiwald db2579467e feat(dns): Hephaestus Protocol surgical DNS PCB override
BREAKTHROUGH: Manual DNS PCB initialization now succeeds!

CRITICAL FIXES:
- Exposed dns_pcbs[] and dns_recv() for external manual setup
- Implemented Hephaestus Protocol surgical override in net_glue.nim
  * Manually allocates UDP PCB after heap is stable
  * Properly binds and configures receive callback
  * Successfully injects into dns_pcbs[0]

VALIDATION:
 Hephaestus override executes successfully
 udp_new() returns valid 48-byte PCB
 udp_bind() succeeds
 Callback configured
 DNS PCB injected

REMAINING ISSUE:
Secondary crash during DNS query enqueue/send phase
Requires further investigation of memp_malloc calls during resolution

Voxis + Hephaestus: The forge burns bright.
2026-01-08 09:27:28 +01:00
Markus Maiwald f9aa11995c feat(membrane): Hardened LwIP memory manager & stabilized DHCP/DNS
PROBLEM RESOLVED: memp_malloc NULL pointer crashes (0x18/0x20 offsets)

CRITICAL FIXES:
- Nuclear fail-safe in memp.c for mission-critical protocol objects
  * Direct heap fallback for UDP_PCB, TCP_PCB, PBUF, SYS_TMR pools
  * Handles ABI/relocation failures in memp_pools[] descriptor array
  * Prevents ALL NULL dereferences in protocol allocation paths

- Iteration-based network heartbeat in net_glue.nim
  * Drives LwIP state machines independent of system clock
  * Resolves DHCP/DNS timeout issues in QEMU/freestanding environments
  * Ensures consistent protocol advancement even with time dilation

- Unified heap configuration (MEMP_MEM_MALLOC=1, LWIP_TIMERS=1)
  * 2MB heap for network operations
  * Disabled LwIP stats to avoid descriptor corruption
  * Increased pool sizes for robustness

VERIFICATION:
 DHCP: Reliable IP acquisition (10.0.2.15)
 ICMP: Full Layer 2 connectivity confirmed
 DNS: Query enqueuing operational (secondary crash isolated)
 VirtIO: 12-byte header alignment maintained

NEXT: Final DNS request table hardening for complete resolution

Voxis Forge Signature: CORRECTNESS > SPEED
2026-01-07 23:47:04 +01:00
Markus Maiwald 831841dc66 test(network): added DNS resolution verification and extended test script
- Updated init.nim with post-fix DNS resolution test (google.com).
- Added test_network_extended.sh with 120s timeout to allow full DHCP/DNS cycle.
- Validates the fix for the UDP PCB pool exhaustion crash.
2026-01-07 21:28:18 +01:00
Markus Maiwald fc7103459d fix(dns): resolved NULL pointer crash by increasing UDP PCB pool
Fixed critical kernel trap (Page Fault at 0x20) occurring during DNS queries.

Root Cause:
- dns_gethostbyname() crashed when accessing NULL udp_pcb pointer
- udp_new_ip_type() failed due to memory pool exhaustion
- MEMP_NUM_UDP_PCB=8 was insufficient (DHCP=1, DNS=1, others=6)

Solution:
- Increased MEMP_NUM_UDP_PCB from 8 to 16 in lwipopts.h
- Added DNS initialization check function in net_glue.nim
- Documented root cause analysis in DNS_NULL_CRASH_RCA.md

Impact:
- System now boots without crashes
- DNS infrastructure stable and ready for queries
- Network stack remains operational under load

Verified: No kernel traps during 60s test run with DHCP + network activity.

Next: Debug DNS query resolution (separate from crash fix).
2026-01-07 21:16:02 +01:00
Markus Maiwald 8acf9644e3 feat(network): established full bidirectional IP connectivity via LwIP
Established stable network link between NexusOS and QEMU/SLIRP gateway.
Resolved critical packet corruption and state machine failures.

Key fixes:
- VIRTIO: Aligned header size to 12 bytes (VIRTIO_NET_F_MRG_RXBUF modern compliance).
- LWIP: Enabled LWIP_TIMERS=1 to drive internal DHCP/DNS state machines.
- KERNEL: Adjusted NetSwitch polling to 10ms to prevent fiber starvation.
- MEMBRANE: Corrected TX packet offset and fixed comment syntax.
- INIT: Verified ICMP Echo Request/Reply (10.0.2.15 <-> 10.0.2.2).

Physically aligned. Logically sovereign.
Fixed by the Voxis & Hephaestus Forge.
2026-01-07 20:19:15 +01:00
Markus Maiwald b1e80047f1 test(utcp): Root cause analysis - QEMU hostfwd requires listening socket
Documented why UDP/9999 packets don't reach Fast Path. QEMU's NAT drops packets without listening socket. Proposed TAP networking solution for Phase 38.
2026-01-07 17:04:51 +01:00
Markus Maiwald e0f7ad2191 feat(utcp): UTCP Protocol Implementation (SPEC-093)
Implemented UtcpHeader (46 bytes) with CellID-based routing. Integrated UTCP handler into NetSwitch Fast Path. UDP/9999 tunnel packets now route to utcp_handle_packet().
2026-01-07 16:45:06 +01:00
Markus Maiwald 08d31f879c feat(net): Fast Path/Zero-Copy Bypass & Network Stack Documentation
Implemented Fast Path filter for UDP/9999 UTCP tunnel traffic, bypassing LwIP stack. Added zero-copy header stripping in fastpath.nim. Documented full network stack architecture in docs/NETWORK_STACK.md. Verified ICMP ping and LwIP graft functionality.
2026-01-07 16:29:15 +01:00
Markus Maiwald de971b465e Network: Phase 36 Component (DHCP, VirtIO 12B, Hardened Logs) 2026-01-07 14:48:40 +01:00
Markus Maiwald bc5f488155 feat(hal/core): implement heartbeat of iron (real-time SBI timer driver)
- Implemented RISC-V SBI timer driver in HAL (entry_riscv.zig).

- Integrated timer into the Harmonic Scheduler (kernel.nim/sched.nim).

- Re-enabled the Silence Doctrine: system now enters low-power WFI state during idle.

- Confirmed precise nanosecond wakeup and LwIP pump loop stability.

- Updated kernel version to v1.1.2.
2026-01-06 20:54:22 +01:00
Markus Maiwald 8729e9b9a4 docs(core): add Network Membrane technical documentation 2026-01-06 18:40:30 +01:00
Markus Maiwald 31a834e086 feat(core): fix userland network init, implement syscalls, bump v1.1.1
- Fix init crash by implementing SYS_WAIT_MULTI and valid hex printing.

- Fix Supervisor Mode hang using busy-wait loop (bypassing missing timer).

- Confirm LwIP Egress transmission and Timer functionality.

- Update kernel version to v1.1.1.
2026-01-06 18:31:32 +01:00
Markus Maiwald d1adf17145 fix(virtio): overcome capability probe hang with paging enabled
- Fixes VirtIO-PCI capability probing logic to handle invalid BAR indices gracefully.
- Enables defensive programming in virtio_pci.zig loop.
- Implements Typed Channel Multiplexing (0x500/0x501) for NetSwitch.
- Grants networking capabilities to Subject/Userland.
- Refactors NexShell to use reactive I/O (ion_wait_multi).
- Bumps version to 2026.1.1 (Patch 1).
2026-01-06 13:39:40 +01:00
Markus Maiwald 09b78d1296 feat(nexshell): implement Visual Causal Graph Viewer
- Added 'stl graph' command to NexShell for ASCII causal visualization
- Integrated Causal Graph Audit into kernel boot summary
- Optimized STL list command to show absolute event IDs
- Fixed Nim kernel crashes by avoiding dynamic string allocations in STL summary
- Hardened HAL-to-NexShell interface with proper extern declarations
2026-01-06 10:13:59 +01:00
Markus Maiwald 76f2578a4b feat(kernel): implement System Truth Ledger and Causal Trace
- Implemented System Ontology (SPEC-060) and STL (SPEC-061) in Zig HAL
- Created Nim bindings and high-level event emission API
- Integrated STL into kernel boot sequence (SystemBoot, FiberSpawn, CapGrant)
- Implemented Causal Graph Engine (SPEC-062) for lineage tracing
- Verified self-aware causal auditing in boot logs
- Optimized Event structure to 58 bytes for cache efficiency
2026-01-06 03:37:53 +01:00
Markus Maiwald 8a4c57b34a feat(kernel): implement Sv39 fiber memory isolation and hardened ELF loader 2026-01-05 16:36:25 +01:00
Markus Maiwald cf93016bd4 feat(rumpk): Implement PTY subsystem for terminal semantics
Phase 40: The Soul Bridge

IMPLEMENTED:
- PTY subsystem with master/slave fd pairs (100-107 / 200-207)
- Ring buffer-based bidirectional I/O (4KB each direction)
- Line discipline (CANON/RAW modes, echo support)
- Integration with FB terminal renderer

CHANGES:
- [NEW] core/pty.nim - Complete PTY implementation
- [MODIFY] kernel.nim - Wire PTY to syscalls, add pty_init() to boot

DATA FLOW:
Keyboard → ION chan_input → pty_push_input → master_to_slave buffer
→ pty_read_slave → mksh stdin → mksh stdout → pty_write_slave
→ term_putc/term_render → Framebuffer

VERIFICATION:
[PTY] Subsystem Initialized
[PTY] Allocated ID=0x0000000000000000
[PTY] Console PTY Allocated

REMAINING: /dev/tty device node for full TTY support

Co-authored-by: Voxis Forge <voxis@nexus-os.org>
2026-01-05 01:39:53 +01:00
Markus Maiwald 8356365610 feat(rumpk): Achieve interactive Mksh shell & formalize Sovereign FSH
CHECKPOINT 7: Nuke LwIP, Fix Stack

🎯 PRIMARY ACHIEVEMENTS:
-  Interactive Mksh shell successfully boots and accepts input
-  Kernel-side LwIP networking disabled (moved to userland intent)
-  C-ABI handover fully operational (argc, argv, environ)
-  SPEC-130: Sovereign Filesystem Hierarchy formalized

🔧 KERNEL FIXES:
1. **Nuked Kernel LwIP**
   - Disabled membrane_init() in kernel.nim
   - Prevented automatic DHCP/IP acquisition
   - Network stack deferred to userland control

2. **Fixed C-ABI Stack Handover**
   - Updated rumpk_enter_userland signature: (entry, argc, argv, sp)
   - Kernel prepares userland stack at 0x8FFFFFE0 (top of user RAM)
   - Stack layout: [argc][argv[0]][argv[1]=NULL][envp[0]=NULL][string data]
   - Preserved kernel-passed arguments through subject_entry.S

3. **Fixed Trap Return Stack Switching**
   - Added sscratch swap before sret in entry_riscv.zig
   - Properly restores user stack and preserves kernel stack pointer
   - Fixes post-syscall instruction page fault

4. **Rebuilt Mksh with Fixed Runtime**
   - subject_entry.S no longer zeros a0/a1
   - Arguments flow: Kernel -> switch.S -> subject_entry.S -> main()

📐 ARCHITECTURAL SPECS:
- **SPEC-130: Sovereign Filesystem Hierarchy**
  - Tri-State (+1) Storage Model: /sysro, /etc, /run, /state
  - Declarative Stateless Doctrine (inspired by Clear Linux/Silverblue)
  - Ghost Writer Pattern: KDL recipes -> /etc generation
  - Bind-Mount Strategy for legacy app grafting
  - Database Contract for /state (transactional, encrypted)

🛠️ DEVELOPER EXPERIENCE:
- Fixed filesystem.nim to fallback to .nexus/ for local builds
- Prevents permission errors during development

🧪 VERIFICATION:

Syscalls confirmed working: write (0x200, 0x204), read (0x203)

NEXT: Implement proper TTY/PTY subsystem for full job control

Co-authored-by: Voxis Forge <voxis@nexus-os.org>
2026-01-05 01:14:24 +01:00
27 changed files with 3518 additions and 14052 deletions

View File

@ -1,131 +0,0 @@
name: Rumpk CI
on:
push:
branches: [unstable, main]
pull_request:
branches: [unstable, main]
jobs:
build-riscv:
runs-on: ubuntu-latest
container:
image: nexus-os/build-env:latest
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Build RISC-V kernel
run: |
echo "Building for RISC-V..."
zig build -Darch=riscv64 -Drelease
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rumpk-riscv64
path: zig-out/rumpk-riscv64.elf
retention-days: 7
build-aarch64:
runs-on: ubuntu-latest
container:
image: nexus-os/build-env:latest
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Build ARM64 kernel
run: |
echo "Building for ARM64..."
zig build -Darch=aarch64 -Drelease
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rumpk-aarch64
path: zig-out/rumpk-aarch64.elf
retention-days: 7
security-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Sensitive content scan
run: |
echo "🔍 Scanning for sensitive content..."
# Check for forbidden directories
if git log --all --name-only | grep -qE '\.agent/|\.vscode/|\.claude/|\.kiro/'; then
echo "❌ CRITICAL: Forbidden directory found in git history"
exit 1
fi
# Check for internal paths
if git log --all -p | grep -qE '/home/markus/zWork/|/home/markus/\.claude/'; then
echo "❌ CRITICAL: Internal path found in git history"
exit 1
fi
echo "✅ No sensitive content detected"
- name: License header check
run: |
echo "Checking license headers..."
# TODO: Implement license header checker
echo " License check pending"
test-qemu:
needs: [build-riscv]
runs-on: ubuntu-latest
container:
image: nexus-os/build-env:latest
options: --privileged
steps:
- uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: rumpk-riscv64
path: zig-out/
- name: QEMU boot test
timeout-minutes: 5
run: |
echo "🚀 Booting RISC-V kernel in QEMU..."
timeout 10s qemu-system-riscv64 \
-machine virt \
-cpu rv64 \
-smp 2 \
-m 128M \
-kernel zig-out/rumpk-riscv64.elf \
-serial stdio \
-display none \
-bios none || echo "Boot test completed"
reproducibility-check:
runs-on: ubuntu-latest
container:
image: nexus-os/build-env:latest
steps:
- uses: actions/checkout@v4
- name: Build twice and compare
run: |
echo "🔧 Building first time..."
zig build -Drelease
cp zig-out/rumpk-riscv64.elf /tmp/build1.elf
echo "🔧 Building second time..."
zig build -Drelease
echo "📊 Comparing builds..."
if diff /tmp/build1.elf zig-out/rumpk-riscv64.elf; then
echo "✅ Reproducible build verified"
else
echo "⚠️ Build not reproducible (timestamps embedded)"
fi

54
.gitignore vendored Normal file
View File

@ -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/

View File

@ -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
}

54
boot/linker_aarch64.ld Normal file
View File

@ -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*)
}
}

125
build_full.sh Executable file
View File

@ -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

61
build_lwip.sh Executable file
View File

@ -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

126
build_nim.sh Executable file
View File

@ -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"

129
core/fs/lfs_bridge.nim Normal file
View File

@ -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)

1060
hal/entry_aarch64.zig Normal file

File diff suppressed because it is too large Load Diff

169
hal/gic.zig Normal file
View File

@ -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);
}

BIN
hal/init

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

362
hal/littlefs_hal.zig Normal file
View File

@ -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);
}

268
hal/virtio_mmio.zig Normal file
View File

@ -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));
}

View File

@ -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]);
}

View File

@ -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);
}

View File

@ -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

View File

@ -0,0 +1,13 @@
#ifndef STDIO_H
#define STDIO_H
#include <stddef.h>
#include <stdarg.h>
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

View File

@ -0,0 +1,10 @@
#ifndef STDLIB_H
#define STDLIB_H
#include <stddef.h>
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

86
run_aarch64.sh Executable file
View File

@ -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

127
vendor/littlefs/lfs_rumpk.h vendored Normal file
View File

@ -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 <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#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

Binary file not shown.

Binary file not shown.

13921
vendor/mksh/mksh/check.t vendored

File diff suppressed because it is too large Load Diff

2
zig-cc-wrapper.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
zig cc -target riscv64-freestanding-none -mcpu=sifive_u54 "$@"