From 8e3375efa3abad13f36dcf2c6b2b8d6eda3784d3 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 10 Dec 2025 13:38:38 +0200 Subject: [PATCH 01/70] metering scaffolding --- crates/vm/src/cpu.rs | 621 +++----------------- crates/vm/src/exe.rs | 738 ++++++++++++++++++++++++ crates/vm/src/instruction.rs | 4 +- crates/vm/src/lib.rs | 3 +- crates/vm/src/memory_page.rs | 121 +++- crates/vm/src/metering.rs | 78 +++ crates/vm/src/sys_call.rs | 105 +++- crates/vm/src/vm.rs | 8 +- crates/vm/tests/allocator_test.rs | 12 +- crates/vm/tests/memory_page_offset.rs | 36 +- crates/vm/tests/test_syscall_handler.rs | 4 +- 11 files changed, 1118 insertions(+), 612 deletions(-) create mode 100644 crates/vm/src/exe.rs create mode 100644 crates/vm/src/metering.rs diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 98e9241..bf3c1cf 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -6,10 +6,11 @@ use std::rc::Rc; use core::cell::RefCell; use crate::host_interface::HostInterface; use crate::sys_call::SyscallHandler; -use crate::registers::Register; use core::fmt::Write; use std::collections::HashMap; -use crate::instruction::CsrOp; +use crate::metering::{Metering, MeterResult, NoopMeter, MemoryAccessKind}; +#[path = "exe.rs"] +mod exec; /// Represents the Central Processing Unit (CPU) of our RISC-V virtual machine. /// @@ -73,6 +74,9 @@ pub struct CPU { /// If None, uses println! to console pub verbose_writer: Option>>, + /// Pluggable metering implementation (gas, resource accounting, etc.) + pub metering: Box, + /// Minimal CSR storage for CSR instructions pub csrs: HashMap, } @@ -85,6 +89,7 @@ impl std::fmt::Debug for CPU { .field("verbose", &self.verbose) .field("reservation_addr", &self.reservation_addr) .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "Some()")) + .field("metering", &"") .finish() } } @@ -100,6 +105,14 @@ impl CPU { /// - All registers start at 0 (except x0 which is always 0) /// - Verbose logging is disabled by default pub fn new(syscall_handler: Box) -> Self { + Self::with_metering(syscall_handler, Box::new(NoopMeter::default())) + } + + /// Creates a new CPU instance with a custom metering implementation. + pub fn with_metering( + syscall_handler: Box, + metering: Box, + ) -> Self { Self { pc: 0, regs: [0; 32], @@ -107,6 +120,7 @@ impl CPU { syscall_handler, reservation_addr: None, verbose_writer: None, + metering, csrs: HashMap::new(), } } @@ -115,6 +129,11 @@ impl CPU { pub fn set_verbose_writer(&mut self, writer: Rc>) { self.verbose_writer = Some(writer); } + + /// Swap in a new metering implementation. + pub fn set_metering(&mut self, metering: Box) { + self.metering = metering; + } /// Helper method to log output /// Only logs if verbose is true and self.verbose is enabled @@ -134,19 +153,30 @@ impl CPU { } } - fn read_csr(&self, csr: u16) -> u32 { + fn can_continue(result: MeterResult) -> bool { + matches!(result, MeterResult::Continue) + } + + fn read_csr(&mut self, csr: u16) -> Option { + if !Self::can_continue(self.metering.on_pc_update(self.pc, self.pc)) { + return None; + } // Provide simple defaults for common CSRs; fall back to stored values or zero. - match csr { + Some(match csr { 0xF14 => *self.csrs.get(&csr).unwrap_or(&0), // mhartid 0xF11 | 0xF12 | 0xF13 => *self.csrs.get(&csr).unwrap_or(&0), // mvendorid/marchid/mimpid 0x301 => *self.csrs.get(&csr).unwrap_or(&0), // misa 0x300 => *self.csrs.get(&csr).unwrap_or(&0), // mstatus _ => *self.csrs.get(&csr).unwrap_or(&0), - } + }) } - fn write_csr(&mut self, csr: u16, value: u32) { + fn write_csr(&mut self, csr: u16, value: u32) -> bool { + if !Self::can_continue(self.metering.on_pc_update(self.pc, self.pc)) { + return false; + } self.csrs.insert(csr, value); + true } /// Executes a single instruction cycle (fetch, decode, execute). @@ -229,7 +259,11 @@ impl CPU { } else { self.log(&format!("PC = 0x{:08x}, Instr = {}", self.pc, instr.pretty_print()), true); } - + + if !Self::can_continue(self.metering.on_instruction(self.pc, &instr, size)) { + return false; + } + // EDUCATIONAL: Remember the old PC to detect if the instruction changed it let old_pc = self.pc; @@ -239,7 +273,9 @@ impl CPU { // EDUCATIONAL: Only increment PC if the instruction didn't change it // This handles branches, jumps, and calls correctly if self.pc == old_pc { - self.pc = self.pc.wrapping_add(size as u32); + if !self.pc_add(size as u32) { + return false; + } } result } @@ -317,545 +353,50 @@ impl CPU { } } - /// Safely write to a register, ignoring writes to x0 (which should always be 0) - fn write_reg(&mut self, rd: usize, value: u32) { - if rd != 0 { - self.regs[rd] = value; + /// Safely read a register with metering. + fn read_reg(&mut self, reg: usize) -> Option { + if !Self::can_continue(self.metering.on_register_read(reg)) { + return None; } - // Writes to x0 are ignored (RISC-V specification) + Some(self.regs[reg]) } - - /// Executes a decoded instruction. - /// - /// EDUCATIONAL PURPOSE: This is the execute phase of the instruction cycle. - /// It contains the implementation of all RISC-V instructions supported by - /// our VM. This is where the actual computation happens. - /// - /// INSTRUCTION CATEGORIES: - /// - Arithmetic: ADD, SUB, MUL, DIV, etc. - /// - Logical: AND, OR, XOR, shifts - /// - Memory: Load and store operations - /// - Control: Branches and jumps - /// - System: System calls and special operations - /// - /// REGISTER CONVENTIONS: - /// - rd: Destination register (where result goes) - /// - rs1, rs2: Source registers (operands) - /// - imm: Immediate value (constant) - /// - /// RETURN VALUE: Returns true to continue execution, false to halt - pub fn execute( - &mut self, - instr: Instruction, - memory: Rc>, - storage: Rc>, - host: &mut Box) -> bool { - match instr { - // EDUCATIONAL: Arithmetic instructions - perform mathematical operations - Instruction::Add { rd, rs1, rs2 } => { - // EDUCATIONAL: Use wrapping_add to handle overflow correctly - // In real CPUs, overflow might set flags or cause exceptions - self.write_reg(rd, self.regs[rs1].wrapping_add(self.regs[rs2])) - } - Instruction::Sub { rd, rs1, rs2 } => { - self.write_reg(rd, self.regs[rs1].wrapping_sub(self.regs[rs2])) - } - Instruction::Addi { rd, rs1, imm } => { - self.write_reg(rd, self.regs[rs1].wrapping_add(imm as u32)) - } - - // EDUCATIONAL: Logical instructions - perform bitwise operations - Instruction::And { rd, rs1, rs2 } => self.write_reg(rd, self.regs[rs1] & self.regs[rs2]), - Instruction::Or { rd, rs1, rs2 } => self.write_reg(rd, self.regs[rs1] | self.regs[rs2]), - Instruction::Xor { rd, rs1, rs2 } => self.write_reg(rd, self.regs[rs1] ^ self.regs[rs2]), - Instruction::Andi { rd, rs1, imm } => self.write_reg(rd, self.regs[rs1] & (imm as u32)), - Instruction::Ori { rd, rs1, imm } => self.write_reg(rd, self.regs[rs1] | (imm as u32)), - Instruction::Xori { rd, rs1, imm } => self.write_reg(rd, self.regs[rs1] ^ (imm as u32)), - - // EDUCATIONAL: Comparison instructions - set result to 0 or 1 - Instruction::Slt { rd, rs1, rs2 } => { - // EDUCATIONAL: Set if less than (signed comparison) - self.write_reg(rd, (self.regs[rs1] as i32).lt(&(self.regs[rs2] as i32)) as u32) - } - Instruction::Sltu { rd, rs1, rs2 } => { - // EDUCATIONAL: Set if less than (unsigned comparison) - self.write_reg(rd, (self.regs[rs1].lt(&self.regs[rs2])) as u32) - } - Instruction::Slti { rd, rs1, imm } => { - self.write_reg(rd, (self.regs[rs1] as i32).lt(&imm) as u32) - } - Instruction::Sltiu { rd, rs1, imm } => { - let lhs = self.regs[rs1]; - let rhs = imm as u32; - self.write_reg(rd, if lhs < rhs { 1 } else { 0 }); - } - - // EDUCATIONAL: Shift instructions - move bits left or right - Instruction::Sll { rd, rs1, rs2 } => { - // EDUCATIONAL: Logical left shift - multiply by 2^shift_amount - // The & 0x1F ensures shift amount is 0-31 (5 bits) - self.write_reg(rd, self.regs[rs1] << (self.regs[rs2] & 0x1F)) - } - Instruction::Srl { rd, rs1, rs2 } => { - // EDUCATIONAL: Logical right shift - divide by 2^shift_amount - self.write_reg(rd, self.regs[rs1] >> (self.regs[rs2] & 0x1F)) - } - Instruction::Sra { rd, rs1, rs2 } => { - // EDUCATIONAL: Arithmetic right shift - preserves sign bit - self.write_reg(rd, ((self.regs[rs1] as i32) >> (self.regs[rs2] & 0x1F)) as u32) - } - Instruction::Slli { rd, rs1, shamt } => self.write_reg(rd, self.regs[rs1] << shamt), - Instruction::Srli { rd, rs1, shamt } => self.write_reg(rd, self.regs[rs1] >> shamt), - Instruction::Srai { rd, rs1, shamt } => { - self.write_reg(rd, ((self.regs[rs1] as i32) >> shamt) as u32) - } - - // EDUCATIONAL: Load instructions - read data from memory into registers - Instruction::Lw { rd, rs1, offset } => { - // EDUCATIONAL: Load word (32-bit) from memory - // Address = base register + offset - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - self.write_reg(rd, memory.borrow().load_u32(addr)); - } - Instruction::Ld { rd, rs1, offset } => { - // EDUCATIONAL: Load doubleword (64-bit) from memory, truncated to 32-bit - // Since this is a 32-bit VM, we only load the lower 32 bits - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - self.write_reg(rd, memory.borrow().load_u32(addr)); - } - Instruction::Lb { rd, rs1, offset } => { - // EDUCATIONAL: Load byte (8-bit, sign-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let byte = memory.borrow().load_byte(addr); - let value = (byte as i8) as i32 as u32; // sign-extend to 32-bit - self.write_reg(rd, value); - } - Instruction::Lbu { rd, rs1, offset } => { - // EDUCATIONAL: Load byte unsigned (8-bit, zero-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let byte = memory.borrow().load_byte(addr); - self.write_reg(rd, byte as u32); - } - Instruction::Lh { rd, rs1, offset } => { - // EDUCATIONAL: Load halfword (16-bit, sign-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let halfword = memory.borrow().load_halfword(addr); - let value = (halfword as i16) as i32 as u32; // sign-extend to 32-bit - self.write_reg(rd, value); - } - Instruction::Lhu { rd, rs1, offset } => { - // EDUCATIONAL: Load halfword unsigned (16-bit, zero-extended) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - let halfword = memory.borrow().load_halfword(addr); - self.write_reg(rd, halfword as u32); // zero-extend to 32-bit - } - - // EDUCATIONAL: Store instructions - write data from registers to memory - Instruction::Sh { rs1, rs2, offset } => { - // EDUCATIONAL: Store halfword (16-bit) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - memory.borrow_mut().store_u16(addr, (self.regs[rs2] & 0xFFFF) as u16); - } - Instruction::Sw { rs1, rs2, offset } => { - // EDUCATIONAL: Store word (32-bit) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - memory.borrow_mut().store_u32(addr, self.regs[rs2]); - } - Instruction::Sb { rs1, rs2, offset } => { - // EDUCATIONAL: Store byte (8-bit) - let addr = self.regs[rs1].wrapping_add(offset as u32) as usize; - memory.borrow_mut().store_u8(addr, (self.regs[rs2] & 0xFF) as u8); - } - - // EDUCATIONAL: Branch instructions - conditionally change the PC - // These implement if/else and loop constructs - Instruction::Beq { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if equal - jump if two registers are equal - if self.regs[rs1] == self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bne { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if not equal - if self.regs[rs1] != self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Blt { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if less than (signed comparison) - if (self.regs[rs1] as i32) < (self.regs[rs2] as i32) { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bge { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if greater than or equal (signed) - if (self.regs[rs1] as i32) >= (self.regs[rs2] as i32) { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bltu { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if less than (unsigned comparison) - if self.regs[rs1] < self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - - Instruction::Bgeu { rs1, rs2, offset } => { - // EDUCATIONAL: Branch if greater than or equal (unsigned) - if self.regs[rs1] >= self.regs[rs2] { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - // EDUCATIONAL: Jump and Link instructions - for function calls - Instruction::Jal { rd, offset, compressed } => { - // EDUCATIONAL: JAL (Jump and Link) - unconditional jump with return address - // Used for function calls and long-distance jumps - // The return address is stored in rd (usually x1/ra) - let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; - self.write_reg(rd, return_address); - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - Instruction::Jalr { rd, rs1, offset , compressed} => { - // EDUCATIONAL: JALR (Jump and Link Register) - indirect function calls - // Target address = base register + offset, with bottom bit cleared - // This ensures proper alignment and is required by RISC-V spec - let base = self.regs[rs1]; - let target = base.wrapping_add(offset as u32) & !1; - - // For compressed instructions (c.jalr), return address should be pc + 2 - // For regular instructions (jalr), return address should be pc + 4 - let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; - - self.write_reg(rd, return_address); - - self.pc = target; - return true; - } - - // EDUCATIONAL: Load Upper Immediate - loads immediate into upper bits - Instruction::Lui { rd, imm } => { - // EDUCATIONAL: LUI loads a 20-bit immediate into bits 31-12 of rd - // This is used to load large constants (like addresses) into registers - self.write_reg(rd, (imm << 12) as u32) - } - Instruction::Auipc { rd, imm } => { - // EDUCATIONAL: AUIPC (Add Upper Immediate to PC) - PC-relative addressing - // Used for position-independent code and loading addresses relative to PC - self.write_reg(rd, self.pc.wrapping_add((imm << 12) as u32)); - } - - // EDUCATIONAL: Multiplication instructions - extended arithmetic - Instruction::Mul { rd, rs1, rs2 } => { - // EDUCATIONAL: MUL - multiply two registers, store lower 32 bits - self.write_reg(rd, self.regs[rs1].wrapping_mul(self.regs[rs2])) - } - Instruction::Mulh { rd, rs1, rs2 } => { - // EDUCATIONAL: MULH - multiply signed, store upper 32 bits - // Properly sign-extend 32-bit values to 64-bit for signed multiplication - let val1 = (self.regs[rs1] as i32) as i64; - let val2 = (self.regs[rs2] as i32) as i64; - let result = val1 * val2; - self.write_reg(rd, (result >> 32) as u32) - } - Instruction::Mulhu { rd, rs1, rs2 } => { - // EDUCATIONAL: MULHU - multiply unsigned, store upper 32 bits - self.write_reg(rd, (((self.regs[rs1] as u64) * (self.regs[rs2] as u64)) >> 32) as u32) - } - Instruction::Mulhsu { rd, rs1, rs2 } => { - // EDUCATIONAL: MULHSU - multiply signed by unsigned, store upper 32 bits - // Properly sign-extend first operand to signed 64-bit, keep second as unsigned 64-bit - let val1 = (self.regs[rs1] as i32) as i64; - let val2 = self.regs[rs2] as u64; - let result = val1 * (val2 as i64); - self.write_reg(rd, (result >> 32) as u32) - } - // EDUCATIONAL: Division and remainder instructions - Instruction::Div { rd, rs1, rs2 } => { - // EDUCATIONAL: DIV - signed division - // RISC-V spec: division by zero returns -1, overflow returns dividend - if self.regs[rs2] == 0 { - self.write_reg(rd, 0xFFFFFFFF); // -1 in two's complement - } else { - let dividend = self.regs[rs1] as i32; - let divisor = self.regs[rs2] as i32; - - // Check for overflow: -2^31 / -1 = 2^31 (overflow) - if dividend == i32::MIN && divisor == -1 { - self.write_reg(rd, self.regs[rs1]); // Return dividend on overflow - } else { - self.write_reg(rd, (dividend / divisor) as u32) - } - } - } - Instruction::Divu { rd, rs1, rs2 } => { - // EDUCATIONAL: DIVU - unsigned division - // RISC-V spec: division by zero returns 2^XLEN - 1 - if self.regs[rs2] == 0 { - self.write_reg(rd, 0xFFFFFFFF); // 2^32 - 1 - } else { - self.write_reg(rd, self.regs[rs1] / self.regs[rs2]) - } - } - Instruction::Rem { rd, rs1, rs2 } => { - // EDUCATIONAL: REM - signed remainder - // RISC-V spec: remainder by zero returns dividend, overflow returns dividend - if self.regs[rs2] == 0 { - self.write_reg(rd, self.regs[rs1]) - } else { - let dividend = self.regs[rs1] as i32; - let divisor = self.regs[rs2] as i32; - - // Check for overflow: -2^31 % -1 = 0 (no overflow, but -2^31 % -1 = 0) - if dividend == i32::MIN && divisor == -1 { - self.write_reg(rd, 0) // Remainder of -2^31 % -1 is 0 - } else { - self.write_reg(rd, (dividend % divisor) as u32) - } - } - } - Instruction::Remu { rd, rs1, rs2 } => { - // EDUCATIONAL: REMU - unsigned remainder - // RISC-V spec: remainder by zero returns dividend - if self.regs[rs2] == 0 { - self.write_reg(rd, self.regs[rs1]) - } else { - self.write_reg(rd, self.regs[rs1] % self.regs[rs2]) - } - } - - // EDUCATIONAL: System instructions - for OS interaction and debugging - Instruction::Ecall => { - // Prepare syscall args from registers - let args = [ - self.regs[Register::A1 as usize], - self.regs[Register::A2 as usize], - self.regs[Register::A3 as usize], - self.regs[Register::A4 as usize], - self.regs[Register::A5 as usize], - self.regs[Register::A6 as usize], - ]; - let call_id = self.regs[Register::A7 as usize]; - let (result, cont) = self.syscall_handler.handle_syscall(call_id, args, memory, storage, host, &mut self.regs); - self.regs[Register::A0 as usize] = result; - return cont; - } - Instruction::Csr { rd, rs1, csr, op, imm } => { - let src = if imm { rs1 as u32 } else { self.regs[rs1] }; - let old = self.read_csr(csr); - - // Apply CSR op semantics - let mut new_val = old; - match op { - CsrOp::Csrrw => { - if !(imm == false && rs1 == 0) { - new_val = src; - } - } - CsrOp::Csrrs => { - if src != 0 { - new_val = old | src; - } - } - CsrOp::Csrrc => { - if src != 0 { - new_val = old & !src; - } - } - } - - if src != 0 || matches!(op, CsrOp::Csrrw) { - self.write_csr(csr, new_val); - } - - if rd != 0 { - self.write_reg(rd, old); - } - } - Instruction::Ebreak => { - // EDUCATIONAL: EBREAK - Environment Break - for debugging - // In real systems, this would trigger a debugger breakpoint - return false - } - Instruction::Mret => { - // Treat MRET as a simple return/halt in this VM + /// Safely write to a register, ignoring writes to x0 (which should always be 0). + /// Returns false if metering halts execution. + fn write_reg(&mut self, rd: usize, value: u32) -> bool { + if rd != 0 { + if !Self::can_continue(self.metering.on_register_write(rd)) { return false; } - - // EDUCATIONAL: Compressed instruction set (RV32C) - space-saving instructions - Instruction::Jr { rs1 } => { - // EDUCATIONAL: JR (Jump Register) - compressed jump to register - self.pc = self.regs[rs1]; - return true; - } - Instruction::Ret => { - // EDUCATIONAL: RET - compressed return instruction - // Equivalent to JR x1 (jump to return address register) - let target = self.regs[1]; // x1 = ra (return address) - if target == 0 || target == 0xFFFF_FFFF { - return false; // halt if ret target is 0 or invalid - } - - self.pc = target; - return true; - } - Instruction::Mv { rd, rs2 } => { - // EDUCATIONAL: MV (Move) - compressed register copy - self.write_reg(rd, self.regs[rs2]) - } - Instruction::Addi16sp { imm } => { - // EDUCATIONAL: ADDI16SP - add immediate to stack pointer - // x2 is the stack pointer (SP) - self.write_reg(2, self.regs[2].wrapping_add(imm as u32)) - } - Instruction::Addi4spn { rd, imm } => { - // EDUCATIONAL: ADDI4SPN - add immediate to SP, store in rd - // Used for stack frame setup in function prologues - self.write_reg(rd, self.regs[2].wrapping_add(imm)); - } - Instruction::Nop => { - // EDUCATIONAL: NOP - No Operation - does nothing - // Used for alignment and timing in real systems - } - Instruction::Beqz { rs1, offset } => { - // EDUCATIONAL: BEQZ - Branch if Equal to Zero (compressed) - if self.regs[rs1] == 0 { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } - Instruction::Bnez { rs1, offset } => { - // EDUCATIONAL: BNEZ - Branch if Not Equal to Zero (compressed) - if self.regs[rs1] != 0 { - self.pc = self.pc.wrapping_add(offset as u32); - return true; - } - } + self.regs[rd] = value; + } + true + } - // EDUCATIONAL: Miscellaneous ALU operations (compressed) - Instruction::MiscAlu { rd, rs2, op } => { - match op { - crate::instruction::MiscAluOp::Sub => { - // EDUCATIONAL: C.SUB - compressed subtract - self.write_reg(rd, self.regs[rd].wrapping_sub(self.regs[rs2])); - } - crate::instruction::MiscAluOp::Xor => { - // EDUCATIONAL: C.XOR - compressed XOR - self.write_reg(rd, self.regs[rd] ^ self.regs[rs2]); - } - crate::instruction::MiscAluOp::Or => { - // EDUCATIONAL: C.OR - compressed OR - self.write_reg(rd, self.regs[rd] | self.regs[rs2]); - } - crate::instruction::MiscAluOp::And => { - // EDUCATIONAL: C.AND - compressed AND - self.write_reg(rd, self.regs[rd] & self.regs[rs2]); - } - } - } - Instruction::Fence => { - // FENCE is a memory barrier in hardware, but is a no-op in this VM - } - Instruction::Unimp => { - // UNIMP is an unimplemented instruction, treat as a no-op for compatibility - } - // ===== RV32A (Atomics) ===== - Instruction::AmoswapW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - memory.borrow_mut().store_u32(addr, self.regs[rs2]); - self.write_reg(rd, orig); - } - Instruction::AmoaddW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig.wrapping_add(self.regs[rs2]); - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmoandW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig & self.regs[rs2]; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmoorW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig | self.regs[rs2]; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmoxorW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = orig ^ self.regs[rs2]; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmomaxW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if (orig as i32) > (self.regs[rs2] as i32) { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmominW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if (orig as i32) < (self.regs[rs2] as i32) { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmomaxuW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if orig > self.regs[rs2] { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - Instruction::AmominuW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let orig = memory.borrow().load_u32(addr); - let new_val = if orig < self.regs[rs2] { orig } else { self.regs[rs2] }; - memory.borrow_mut().store_u32(addr, new_val); - self.write_reg(rd, orig); - } - // ===== RV32A (LR/SC) ===== - Instruction::LrW { rd, rs1 } => { - let addr = self.regs[rs1] as usize; - let value = memory.borrow().load_u32(addr); - self.write_reg(rd, value); - // Set reservation for this address - self.reservation_addr = Some(addr); - } - Instruction::ScW { rd, rs1, rs2 } => { - let addr = self.regs[rs1] as usize; - let value_to_store = self.regs[rs2]; - - // Check if we have a valid reservation for this address - if self.reservation_addr == Some(addr) { - // Reservation is valid, perform the store - memory.borrow_mut().store_u32(addr, value_to_store); - self.write_reg(rd, 0); // 0 = success - // Clear the reservation (it's consumed) - self.reservation_addr = None; - } else { - // No valid reservation, fail - self.write_reg(rd, 1); // 1 = failure - } - } - _ => todo!("unhandled instruction"), + /// Add to the program counter with wrapping semantics and metering. + fn pc_add(&mut self, delta: u32) -> bool { + let old = self.pc; + let new_pc = self.pc.wrapping_add(delta); + if !Self::can_continue(self.metering.on_pc_update(old, new_pc)) { + return false; } + self.pc = new_pc; true + } + + /// Add to the stack pointer (x2) with metering. + fn sp_add(&mut self, delta: u32) -> bool { + let sp = match self.read_reg(2) { Some(v) => v, None => return false }; + self.write_reg(2, sp.wrapping_add(delta)) + } - } + /// Set the program counter and meter the update. + fn set_pc(&mut self, target: u32) -> bool { + let old = self.pc; + if !Self::can_continue(self.metering.on_pc_update(old, target)) { + return false; + } + self.pc = target; + true + } } diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs new file mode 100644 index 0000000..9bbfd73 --- /dev/null +++ b/crates/vm/src/exe.rs @@ -0,0 +1,738 @@ +use std::rc::Rc; +use core::cell::RefCell; + +use super::{CPU, Instruction, MemoryAccessKind, MemoryPage}; +use crate::host_interface::HostInterface; +use crate::instruction::CsrOp; +use crate::registers::Register; +use storage::Storage; + +impl CPU { + /// Executes a decoded instruction. + /// + /// EDUCATIONAL PURPOSE: This is the execute phase of the instruction cycle. + /// It contains the implementation of all RISC-V instructions supported by + /// our VM. This is where the actual computation happens. + /// + /// INSTRUCTION CATEGORIES: + /// - Arithmetic: ADD, SUB, MUL, DIV, etc. + /// - Logical: AND, OR, XOR, shifts + /// - Memory: Load and store operations + /// - Control: Branches and jumps + /// - System: System calls and special operations + /// + /// REGISTER CONVENTIONS: + /// - rd: Destination register (where result goes) + /// - rs1, rs2: Source registers (operands) + /// - imm: Immediate value (constant) + /// + /// RETURN VALUE: Returns true to continue execution, false to halt + pub fn execute( + &mut self, + instr: Instruction, + memory: Rc>, + storage: Rc>, + host: &mut Box, + ) -> bool { + match instr { + // EDUCATIONAL: Arithmetic instructions - perform mathematical operations + Instruction::Add { rd, rs1, rs2 } => { + // EDUCATIONAL: Use wrapping_add to handle overflow correctly + // In real CPUs, overflow might set flags or cause exceptions + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs.wrapping_add(rhs)) { return false; } + } + Instruction::Sub { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { return false; } + } + Instruction::Addi { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs.wrapping_add(imm as u32)) { return false; } + } + + // EDUCATIONAL: Logical instructions - perform bitwise operations + Instruction::And { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs & rhs) { return false; } + } + Instruction::Or { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs | rhs) { return false; } + } + Instruction::Xor { rd, rs1, rs2 } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs ^ rhs) { return false; } + } + Instruction::Andi { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs & (imm as u32)) { return false; } + } + Instruction::Ori { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs | (imm as u32)) { return false; } + } + Instruction::Xori { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs ^ (imm as u32)) { return false; } + } + + // EDUCATIONAL: Comparison instructions - set result to 0 or 1 + Instruction::Slt { rd, rs1, rs2 } => { + // EDUCATIONAL: Set if less than (signed comparison) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, (lhs as i32).lt(&(rhs as i32)) as u32) { return false; } + } + Instruction::Sltu { rd, rs1, rs2 } => { + // EDUCATIONAL: Set if less than (unsigned comparison) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, (lhs < rhs) as u32) { return false; } + } + Instruction::Slti { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, (lhs as i32).lt(&imm) as u32) { return false; } + } + Instruction::Sltiu { rd, rs1, imm } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = imm as u32; + if !self.write_reg(rd, if lhs < rhs { 1 } else { 0 }) { return false; } + } + + // EDUCATIONAL: Shift instructions - move bits left or right + Instruction::Sll { rd, rs1, rs2 } => { + // EDUCATIONAL: Logical left shift - multiply by 2^shift_amount + // The & 0x1F ensures shift amount is 0-31 (5 bits) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs << (rhs & 0x1F)) { return false; } + } + Instruction::Srl { rd, rs1, rs2 } => { + // EDUCATIONAL: Logical right shift - divide by 2^shift_amount + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs >> (rhs & 0x1F)) { return false; } + } + Instruction::Sra { rd, rs1, rs2 } => { + // EDUCATIONAL: Arithmetic right shift - preserves sign bit + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, ((lhs as i32) >> (rhs & 0x1F)) as u32) { return false; } + } + Instruction::Slli { rd, rs1, shamt } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs << shamt) { return false; } + } + Instruction::Srli { rd, rs1, shamt } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs >> shamt) { return false; } + } + Instruction::Srai { rd, rs1, shamt } => { + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.write_reg(rd, ((lhs as i32) >> shamt) as u32) { return false; } + } + + // EDUCATIONAL: Load instructions - read data from memory into registers + Instruction::Lw { rd, rs1, offset } => { + // EDUCATIONAL: Load word (32-bit) from memory + // Address = base register + offset + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let val = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, val) { return false; } + } + Instruction::Ld { rd, rs1, offset } => { + // EDUCATIONAL: Load doubleword (64-bit) from memory, truncated to 32-bit + // Since this is a 32-bit VM, we only load the lower 32 bits + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let val = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, val) { return false; } + } + Instruction::Lb { rd, rs1, offset } => { + // EDUCATIONAL: Load byte (8-bit, sign-extended) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let byte = match memory.borrow().load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + let value = (byte as i8) as i32 as u32; // sign-extend to 32-bit + if !self.write_reg(rd, value) { return false; } + } + Instruction::Lbu { rd, rs1, offset } => { + // EDUCATIONAL: Load byte unsigned (8-bit, zero-extended) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let byte = match memory.borrow().load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, byte as u32) { return false; } + } + Instruction::Lh { rd, rs1, offset } => { + // EDUCATIONAL: Load halfword (16-bit, sign-extended) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let halfword = match memory.borrow().load_halfword(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + let value = (halfword as i16) as i32 as u32; // sign-extend to 32-bit + if !self.write_reg(rd, value) { return false; } + } + Instruction::Lhu { rd, rs1, offset } => { + // EDUCATIONAL: Load halfword unsigned (16-bit, zero-extended) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let halfword = match memory.borrow().load_halfword(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, halfword as u32) { return false; } // zero-extend to 32-bit + } + + // EDUCATIONAL: Store instructions - write data from registers to memory + Instruction::Sh { rs1, rs2, offset } => { + // EDUCATIONAL: Store halfword (16-bit) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !memory.borrow().store_u16(addr, (src & 0xFFFF) as u16, self.metering.as_mut(), MemoryAccessKind::Store) { + return false; + } + } + Instruction::Sw { rs1, rs2, offset } => { + // EDUCATIONAL: Store word (32-bit) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !memory.borrow().store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Store) { + return false; + } + } + Instruction::Sb { rs1, rs2, offset } => { + // EDUCATIONAL: Store byte (8-bit) + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base.wrapping_add(offset as u32) as usize; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !memory.borrow().store_u8(addr, (src & 0xFF) as u8, self.metering.as_mut(), MemoryAccessKind::Store) { + return false; + } + } + + // EDUCATIONAL: Branch instructions - conditionally change the PC + // These implement if/else and loop constructs + Instruction::Beq { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if equal - jump if two registers are equal + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if lhs == rhs { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + Instruction::Bne { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if not equal + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if lhs != rhs { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + Instruction::Blt { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if less than (signed comparison) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if (lhs as i32) < (rhs as i32) { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + Instruction::Bge { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if greater than or equal (signed) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if (lhs as i32) >= (rhs as i32) { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + Instruction::Bltu { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if less than (unsigned comparison) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if lhs < rhs { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + + Instruction::Bgeu { rs1, rs2, offset } => { + // EDUCATIONAL: Branch if greater than or equal (unsigned) + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if lhs >= rhs { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + // EDUCATIONAL: Jump and Link instructions - for function calls + Instruction::Jal { rd, offset, compressed } => { + // EDUCATIONAL: JAL (Jump and Link) - unconditional jump with return address + // Used for function calls and long-distance jumps + // The return address is stored in rd (usually x1/ra) + let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; + if !self.write_reg(rd, return_address) { return false; } + if !self.pc_add(offset as u32) { return false; } + return true; + } + Instruction::Jalr { rd, rs1, offset , compressed} => { + // EDUCATIONAL: JALR (Jump and Link Register) - indirect function calls + // Target address = base register + offset, with bottom bit cleared + // This ensures proper alignment and is required by RISC-V spec + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let target = base.wrapping_add(offset as u32) & !1; + + // For compressed instructions (c.jalr), return address should be pc + 2 + // For regular instructions (jalr), return address should be pc + 4 + let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; + + if !self.write_reg(rd, return_address) { return false; } + + if !self.set_pc(target) { return false; } + return true; + } + + // EDUCATIONAL: Load Upper Immediate - loads immediate into upper bits + Instruction::Lui { rd, imm } => { + // EDUCATIONAL: LUI loads a 20-bit immediate into bits 31-12 of rd + // This is used to load large constants (like addresses) into registers + if !self.write_reg(rd, (imm << 12) as u32) { return false; } + } + Instruction::Auipc { rd, imm } => { + // EDUCATIONAL: AUIPC (Add Upper Immediate to PC) - PC-relative addressing + // Used for position-independent code and loading addresses relative to PC + if !self.write_reg(rd, self.pc.wrapping_add((imm << 12) as u32)) { return false; } + } + + // EDUCATIONAL: Multiplication instructions - extended arithmetic + Instruction::Mul { rd, rs1, rs2 } => { + // EDUCATIONAL: MUL - multiply two registers, store lower 32 bits + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs.wrapping_mul(rhs)) { return false; } + } + Instruction::Mulh { rd, rs1, rs2 } => { + // EDUCATIONAL: MULH - multiply signed, store upper 32 bits + // Properly sign-extend 32-bit values to 64-bit for signed multiplication + let val1 = (match self.read_reg(rs1) { Some(v) => v, None => return false } as i32) as i64; + let val2 = (match self.read_reg(rs2) { Some(v) => v, None => return false } as i32) as i64; + let result = val1 * val2; + if !self.write_reg(rd, (result >> 32) as u32) { return false; } + } + Instruction::Mulhu { rd, rs1, rs2 } => { + // EDUCATIONAL: MULHU - multiply unsigned, store upper 32 bits + let lhs = match self.read_reg(rs1) { Some(v) => v as u64, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v as u64, None => return false }; + if !self.write_reg(rd, ((lhs * rhs) >> 32) as u32) { return false; } + } + Instruction::Mulhsu { rd, rs1, rs2 } => { + // EDUCATIONAL: MULHSU - multiply signed by unsigned, store upper 32 bits + // Properly sign-extend first operand to signed 64-bit, keep second as unsigned 64-bit + let val1 = (match self.read_reg(rs1) { Some(v) => v, None => return false } as i32) as i64; + let val2 = match self.read_reg(rs2) { Some(v) => v as u64, None => return false }; + let result = val1 * (val2 as i64); + if !self.write_reg(rd, (result >> 32) as u32) { return false; } + } + // EDUCATIONAL: Division and remainder instructions + Instruction::Div { rd, rs1, rs2 } => { + // EDUCATIONAL: DIV - signed division + // RISC-V spec: division by zero returns -1, overflow returns dividend + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if rhs == 0 { + if !self.write_reg(rd, 0xFFFFFFFF) { return false; } // -1 in two's complement + } else { + let dividend = lhs as i32; + let divisor = rhs as i32; + + // Check for overflow: -2^31 / -1 = 2^31 (overflow) + if dividend == i32::MIN && divisor == -1 { + if !self.write_reg(rd, lhs) { return false; } // Return dividend on overflow + } else { + if !self.write_reg(rd, (dividend / divisor) as u32) { return false; } + } + } + } + Instruction::Divu { rd, rs1, rs2 } => { + // EDUCATIONAL: DIVU - unsigned division + // RISC-V spec: division by zero returns 2^XLEN - 1 + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if rhs == 0 { + if !self.write_reg(rd, 0xFFFFFFFF) { return false; } // 2^32 - 1 + } else { + if !self.write_reg(rd, lhs / rhs) { return false; } + } + } + Instruction::Rem { rd, rs1, rs2 } => { + // EDUCATIONAL: REM - signed remainder + // RISC-V spec: remainder by zero returns dividend, overflow returns dividend + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if rhs == 0 { + if !self.write_reg(rd, lhs) { return false; } + } else { + let dividend = lhs as i32; + let divisor = rhs as i32; + + // Check for overflow: -2^31 % -1 = 0 (no overflow, but -2^31 % -1 = 0) + if dividend == i32::MIN && divisor == -1 { + if !self.write_reg(rd, 0) { return false; } // Remainder of -2^31 % -1 is 0 + } else { + if !self.write_reg(rd, (dividend % divisor) as u32) { return false; } + } + } + } + Instruction::Remu { rd, rs1, rs2 } => { + // EDUCATIONAL: REMU - unsigned remainder + // RISC-V spec: remainder by zero returns dividend + let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if rhs == 0 { + if !self.write_reg(rd, lhs) { return false; } + } else { + if !self.write_reg(rd, lhs % rhs) { return false; } + } + } + + // EDUCATIONAL: System instructions - for OS interaction and debugging + Instruction::Ecall => { + // Prepare syscall args from registers + let args = [ + match self.read_reg(Register::A1 as usize) { Some(v) => v, None => return false }, + match self.read_reg(Register::A2 as usize) { Some(v) => v, None => return false }, + match self.read_reg(Register::A3 as usize) { Some(v) => v, None => return false }, + match self.read_reg(Register::A4 as usize) { Some(v) => v, None => return false }, + match self.read_reg(Register::A5 as usize) { Some(v) => v, None => return false }, + match self.read_reg(Register::A6 as usize) { Some(v) => v, None => return false }, + ]; + let call_id = match self.read_reg(Register::A7 as usize) { Some(v) => v, None => return false }; + let (result, cont) = self.syscall_handler.handle_syscall( + call_id, + args, + memory, + storage, + host, + &mut self.regs, + self.metering.as_mut(), + ); + if !self.write_reg(Register::A0 as usize, result) { return false; } + return cont; + } + Instruction::Csr { rd, rs1, csr, op, imm } => { + let src = if imm { rs1 as u32 } else { match self.read_reg(rs1) { Some(v) => v, None => return false } }; + let old = match self.read_csr(csr) { Some(v) => v, None => return false }; + + // Apply CSR op semantics + let mut new_val = old; + match op { + CsrOp::Csrrw => { + if !(imm == false && rs1 == 0) { + new_val = src; + } + } + CsrOp::Csrrs => { + if src != 0 { + new_val = old | src; + } + } + CsrOp::Csrrc => { + if src != 0 { + new_val = old & !src; + } + } + } + + if src != 0 || matches!(op, CsrOp::Csrrw) { + if !self.write_csr(csr, new_val) { return false; } + } + + if rd != 0 && !self.write_reg(rd, old) { return false; } + } + Instruction::Ebreak => { + // EDUCATIONAL: EBREAK - Environment Break - for debugging + // In real systems, this would trigger a debugger breakpoint + return false + } + Instruction::Mret => { + // Treat MRET as a simple return/halt in this VM + return false; + } + + // EDUCATIONAL: Compressed instruction set (RV32C) - space-saving instructions + Instruction::Jr { rs1 } => { + // EDUCATIONAL: JR (Jump Register) - compressed jump to register + let target = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if !self.set_pc(target) { return false; } + return true; + } + Instruction::Ret => { + // EDUCATIONAL: RET - compressed return instruction + // Equivalent to JR x1 (jump to return address register) + let target = match self.read_reg(1) { Some(v) => v, None => return false }; // x1 = ra (return address) + if target == 0 || target == 0xFFFF_FFFF { + return false; // halt if ret target is 0 or invalid + } + + if !self.set_pc(target) { return false; } + return true; + } + Instruction::Mv { rd, rs2 } => { + // EDUCATIONAL: MV (Move) - compressed register copy + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, src) { return false; } + } + Instruction::Addi16sp { imm } => { + // EDUCATIONAL: ADDI16SP - add immediate to stack pointer + // x2 is the stack pointer (SP) + if !self.sp_add(imm as u32) { return false; } + } + Instruction::Addi4spn { rd, imm } => { + // EDUCATIONAL: ADDI4SPN - add immediate to SP, store in rd + // Used for stack frame setup in function prologues + let sp = match self.read_reg(2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, sp.wrapping_add(imm)) { return false; } + } + Instruction::Nop => { + // EDUCATIONAL: NOP - No Operation - does nothing + // Used for alignment and timing in real systems + } + Instruction::Beqz { rs1, offset } => { + // EDUCATIONAL: BEQZ - Branch if Equal to Zero (compressed) + let val = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if val == 0 { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + Instruction::Bnez { rs1, offset } => { + // EDUCATIONAL: BNEZ - Branch if Not Equal to Zero (compressed) + let val = match self.read_reg(rs1) { Some(v) => v, None => return false }; + if val != 0 { + if !self.pc_add(offset as u32) { return false; } + return true; + } + } + + // EDUCATIONAL: Miscellaneous ALU operations (compressed) + Instruction::MiscAlu { rd, rs2, op } => { + match op { + crate::instruction::MiscAluOp::Sub => { + // EDUCATIONAL: C.SUB - compressed subtract + let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { return false; } + } + crate::instruction::MiscAluOp::Xor => { + // EDUCATIONAL: C.XOR - compressed XOR + let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs ^ rhs) { return false; } + } + crate::instruction::MiscAluOp::Or => { + // EDUCATIONAL: C.OR - compressed OR + let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs | rhs) { return false; } + } + crate::instruction::MiscAluOp::And => { + // EDUCATIONAL: C.AND - compressed AND + let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; + let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + if !self.write_reg(rd, lhs & rhs) { return false; } + } + } + } + Instruction::Fence => { + // FENCE is a memory barrier in hardware, but is a no-op in this VM + } + Instruction::Unimp => { + // UNIMP is an unimplemented instruction, treat as a no-op for compatibility + } + // ===== RV32A (Atomics) ===== + Instruction::AmoswapW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + if !memory.borrow().store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmoaddW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig.wrapping_add(src); + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmoandW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig & src; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmoorW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig | src; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmoxorW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = orig ^ src; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmomaxW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if (orig as i32) > (src as i32) { orig } else { src }; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmominW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if (orig as i32) < (src as i32) { orig } else { src }; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmomaxuW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if orig > src { orig } else { src }; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + Instruction::AmominuW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let addr = base as usize; + let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if orig < src { orig } else { src }; + if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } + } + // ===== RV32A (LR/SC) ===== + Instruction::LrW { rd, rs1 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base as usize; + let value = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::ReservationLoad) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, value) { return false; } + // Set reservation for this address + self.reservation_addr = Some(addr); + } + Instruction::ScW { rd, rs1, rs2 } => { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let addr = base as usize; + let value_to_store = match self.read_reg(rs2) { Some(v) => v, None => return false }; + + // Check if we have a valid reservation for this address + if self.reservation_addr == Some(addr) { + // Reservation is valid, perform the store + if !memory.borrow().store_u32(addr, value_to_store, self.metering.as_mut(), MemoryAccessKind::ReservationStore) { + return false; + } + if !self.write_reg(rd, 0) { return false; } // 0 = success + // Clear the reservation (it's consumed) + self.reservation_addr = None; + } else { + // No valid reservation, fail + if !self.write_reg(rd, 1) { return false; } // 1 = failure + } + } + _ => todo!("unhandled instruction"), + } + true + } +} diff --git a/crates/vm/src/instruction.rs b/crates/vm/src/instruction.rs index fcfe3cc..e144d2e 100644 --- a/crates/vm/src/instruction.rs +++ b/crates/vm/src/instruction.rs @@ -34,7 +34,7 @@ /// PERFORMANCE IMPLICATIONS: Different instruction types have different /// execution costs. Memory operations are typically slower than register /// operations, and branches can cause pipeline stalls in real CPUs. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Instruction { // ===== RV32I ===== @@ -486,7 +486,7 @@ pub enum CsrOp { /// EDUCATIONAL: Miscellaneous ALU operations for compressed instructions. /// These represent the different operations that can be performed by the C.MISC-ALU instruction. /// Each operation is a 16-bit compressed version of a corresponding 32-bit instruction. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum MiscAluOp { /// C.SUB: rd = rd - rs2 (compressed subtract) /// EDUCATIONAL: Compressed subtract operation. Subtracts rs2 from rd and stores result in rd. diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index ffc1dfe..ff0238b 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -7,4 +7,5 @@ pub mod cpu; pub mod registers; pub mod memory_page; pub mod sys_call; -pub mod host_interface; \ No newline at end of file +pub mod host_interface; +pub mod metering; diff --git a/crates/vm/src/memory_page.rs b/crates/vm/src/memory_page.rs index 23ab24a..f3756d9 100644 --- a/crates/vm/src/memory_page.rs +++ b/crates/vm/src/memory_page.rs @@ -1,12 +1,13 @@ use std::rc::Rc; use std::cell::{RefCell, Cell}; use std::convert::TryInto; +use crate::metering::{Metering, MeterResult, MemoryAccessKind}; #[derive(Debug, Clone)] pub struct MemoryPage { mem: Rc>>, pub next_heap: Cell, - pub base_address: usize, // New: base address for guest memory mapping + pub base_address: usize, // base address for guest memory mapping } pub const HEAP_PTR_OFFSET: u32 = 0x100; @@ -36,76 +37,129 @@ impl MemoryPage { addr.checked_sub(self.base_address).expect("Address below base_address") } - pub fn store_u16(&self, addr: usize, val: u16) { + fn meter_access( + metering: &mut dyn Metering, + kind: MemoryAccessKind, + addr: usize, + bytes: usize, + ) -> bool { + matches!(metering.on_memory_access(kind, addr, bytes), MeterResult::Continue) + } + + pub fn store_u16( + &self, + addr: usize, + val: u16, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 2) { + return false; + } let offset = self.offset(addr); let mut mem = self.mem.borrow_mut(); if offset + 2 > mem.len() { panic!("store u16 out of bounds: addr = 0x{:08x}", addr); } mem[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); + true } - pub fn store_u32(&self, addr: usize, val: u32) { + pub fn store_u32( + &self, + addr: usize, + val: u32, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 4) { + return false; + } let offset = self.offset(addr); let mut mem = self.mem.borrow_mut(); if offset + 4 > mem.len() { panic!("store u32 out of bounds: addr = 0x{:08x}", addr); } mem[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); - } - - pub fn store_u8(&self, addr: usize, val: u8) { + true + } + + pub fn store_u8( + &self, + addr: usize, + val: u8, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 1) { + return false; + } let offset = self.offset(addr); let mut mem = self.mem.borrow_mut(); if offset >= mem.len() { panic!("store u8 out of bounds: addr = 0x{:08x}", addr); } mem[offset] = val; + true } - pub fn load_u32(&self, addr: usize) -> u32 { + pub fn load_u32( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } let offset = self.offset(addr); let mem = self.mem.borrow(); if offset + 4 > mem.len() { panic!("load u32 out of bounds: addr = 0x{:08x}", addr); } - u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap()) + Some(u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap())) } - pub fn load_byte(&self, addr: usize) -> u8 { + pub fn load_byte( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 1) { + return None; + } let offset = self.offset(addr); let mem = self.mem.borrow(); - mem[offset] + Some(mem[offset]) } - pub fn load_halfword(&self, addr: usize) -> u16 { + pub fn load_halfword( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 2) { + return None; + } let offset = self.offset(addr); let mem = self.mem.borrow(); - u16::from_le_bytes(mem[offset..offset + 2].try_into().unwrap()) + Some(u16::from_le_bytes(mem[offset..offset + 2].try_into().unwrap())) } - pub fn load_word(&self, addr: usize) -> u32 { + pub fn load_word( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } let offset = self.offset(addr); let mem = self.mem.borrow(); - u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap()) - } - - pub fn store_byte(&mut self, addr: usize, value: u8) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - mem[offset] = value; - } - - pub fn store_halfword(&mut self, addr: usize, value: u16) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - mem[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); - } - - pub fn store_word(&mut self, addr: usize, value: u32) { - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - mem[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + Some(u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap())) } pub fn mem_slice(&self, start: usize, end: usize) -> Option> { @@ -158,13 +212,16 @@ impl Default for MemoryPage { #[cfg(test)] mod tests { use super::*; + use crate::metering::NoopMeter; #[test] fn test_offset_zero_base() { let mem = MemoryPage::new_with_base(1024, 0); + let mut meter = NoopMeter::default(); assert_eq!(mem.offset(0), 0); assert_eq!(mem.offset(100), 100); assert_eq!(mem.offset(1023), 1023); + assert_eq!(mem.load_u32(0, &mut meter, MemoryAccessKind::Load), Some(0)); } #[test] diff --git a/crates/vm/src/metering.rs b/crates/vm/src/metering.rs new file mode 100644 index 0000000..e35c6a2 --- /dev/null +++ b/crates/vm/src/metering.rs @@ -0,0 +1,78 @@ +use crate::instruction::Instruction; + +/// Outcome returned by metering hooks to indicate whether execution should continue. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MeterResult { + Continue, + Halt, +} + +/// Identifies the type of memory access being charged. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemoryAccessKind { + Load, + Store, + Atomic, + ReservationLoad, + ReservationStore, +} + +/// Pluggable metering interface. Implementors can account for gas or other resource +/// usage without changing the VM core. All methods default to no-op/continue. +pub trait Metering: std::fmt::Debug { + /// Called on instruction execution. + fn on_instruction(&mut self, _pc: u32, _instr: &Instruction, _size: u8) -> MeterResult { + MeterResult::Continue + } + + /// Called for each memory access with its width. + fn on_memory_access( + &mut self, + _kind: MemoryAccessKind, + _addr: usize, + _bytes: usize, + ) -> MeterResult { + MeterResult::Continue + } + + /// Called when a syscall is dispatched (before handler-specific work). + fn on_syscall(&mut self, _call_id: u32, _args: &[u32; 6]) -> MeterResult { + MeterResult::Continue + } + + /// Called for syscall-specific data-dependent charges (payload copies, etc.). + fn on_syscall_data(&mut self, _call_id: u32, _bytes: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when a general-purpose register is read. + fn on_register_read(&mut self, _reg: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when a general-purpose register is written. + fn on_register_write(&mut self, _reg: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when the program counter is updated. + fn on_pc_update(&mut self, _old_pc: u32, _new_pc: u32) -> MeterResult { + MeterResult::Continue + } + + /// Called when guest requests heap allocation. + fn on_alloc(&mut self, _bytes: usize) -> MeterResult { + MeterResult::Continue + } + + /// Called when guest requests a program call; input_bytes covers calldata size. + fn on_call(&mut self, _input_bytes: usize) -> MeterResult { + MeterResult::Continue + } +} + +/// Default metering that performs no accounting. +#[derive(Debug, Default)] +pub struct NoopMeter; + +impl Metering for NoopMeter {} diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index 906aa9f..b9565bc 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -7,6 +7,7 @@ use crate::host_interface::HostInterface; use std::any::Any; use types::result::RESULT_SIZE; use core::fmt::Write; +use crate::metering::{Metering, MeterResult}; /// System call IDs for the VM. pub const SYSCALL_STORAGE_GET: u32 = 1; @@ -41,6 +42,7 @@ pub trait SyscallHandler: std::fmt::Debug { storage: Rc>, host: &mut Box, regs: &mut [u32; 32], + metering: &mut dyn Metering, ) -> (u32, bool); fn as_any(&self) -> &dyn Any; } @@ -80,18 +82,22 @@ impl SyscallHandler for DefaultSyscallHandler { storage: Rc>, host: &mut Box, regs: &mut [u32; 32], + metering: &mut dyn Metering, ) -> (u32, bool) { + if matches!(metering.on_syscall(call_id, &args), MeterResult::Halt) { + panic!("Metering halted syscall {}", call_id); + } let result = match call_id { - SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, storage), - SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, storage), + SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, storage, metering), + SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, storage, metering), SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), - SYSCALL_LOG => self.sys_log(args, memory), - SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host), - SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host), - SYSCALL_ALLOC => self.sys_alloc(args, memory), - SYSCALL_DEALLOC => self.sys_dealloc(args, memory), - SYSCALL_TRANSFER => self.sys_transfer(args, memory, host), - SYSCALL_BALANCE => self.sys_balance(args, memory, host), + SYSCALL_LOG => self.sys_log(args, memory, metering), + SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host, metering), + SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host, metering), + SYSCALL_ALLOC => self.sys_alloc(args, memory, metering), + SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), + SYSCALL_TRANSFER => self.sys_transfer(args, memory, host, metering), + SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), _ => { panic!("Unknown syscall: {}", call_id); } @@ -104,11 +110,19 @@ impl SyscallHandler for DefaultSyscallHandler { } impl DefaultSyscallHandler { - pub fn sys_fire_event(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box,) -> u32 { + pub fn sys_fire_event( + &mut self, args: [u32; 6], + memory: Rc>, + host: &mut Box, + metering: &mut dyn Metering) -> u32 { // EDUCATIONAL: Extract key pointer and length from arguments let ptr = args[0] as usize; let len = args[1] as usize; + if matches!(metering.on_syscall_data(SYSCALL_FIRE_EVENT, len), MeterResult::Halt) { + panic!("Metering halted SYSCALL_FIRE_EVENT"); + } + let borrowed_memory = memory.borrow(); // EDUCATIONAL: Safely read the key from memory @@ -122,11 +136,21 @@ impl DefaultSyscallHandler { 0 } - fn sys_storage_get(&mut self, args: [u32; 6], memory: Rc>, storage: Rc>) -> u32 { + fn sys_storage_get( + &mut self, + args: [u32; 6], + memory: Rc>, + storage: Rc>, + metering: &mut dyn Metering) -> u32 { let domain_ptr = args[0] as usize; let domain_len = args[1] as usize; let key_ptr = args[2] as usize; let key_len = args[3] as usize; + + let total_len = domain_len.saturating_add(key_len); + if matches!(metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), MeterResult::Halt) { + panic!("Metering halted SYSCALL_STORAGE_GET"); + } let borrowed_memory = memory.borrow(); @@ -178,6 +202,9 @@ impl DefaultSyscallHandler { if let Some(value) = storage.borrow().get(domain, &key) { let mut buf = (value.len() as u32).to_le_bytes().to_vec(); buf.extend_from_slice(value.as_slice()); + if matches!(metering.on_alloc(buf.len()), MeterResult::Halt) { + panic!("Metering halted alloc during storage_get"); + } let addr = borrowed_memory.alloc_on_heap(&buf); println!("✅ Found value for domain: '{}', Key: '{}'", domain, display_key); return addr; @@ -187,13 +214,25 @@ impl DefaultSyscallHandler { } } - fn sys_storage_set(&mut self, args: [u32; 6], memory: Rc>, storage: Rc>) -> u32 { + fn sys_storage_set( + &mut self, + args: [u32; 6], + memory: Rc>, + storage: Rc>, + metering: &mut dyn Metering) -> u32 { let domain_ptr = args[0] as usize; let domain_len = args[1] as usize; let key_ptr = args[2] as usize; let key_len = args[3] as usize; let val_ptr = args[4] as usize; let val_len = args[5] as usize; + + let total_len = domain_len + .saturating_add(key_len) + .saturating_add(val_len); + if matches!(metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), MeterResult::Halt) { + panic!("Metering halted SYSCALL_STORAGE_SET"); + } let borrowed_memory = memory.borrow(); @@ -255,7 +294,10 @@ impl DefaultSyscallHandler { 0 } - fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Rc>) -> u32 { + fn sys_panic_with_message( + &mut self, + regs: &mut [u32; 32], + memory: Rc>) -> u32 { let msg_ptr = regs[Register::A0 as usize] as usize; let msg_len = regs[Register::A1 as usize] as usize; let msg = memory @@ -268,8 +310,12 @@ impl DefaultSyscallHandler { panic!("🔥 Guest panic: {}", msg); } - fn sys_log(&mut self, args: [u32; 6], memory: Rc>) -> u32 { + fn sys_log(&mut self, args: [u32; 6], memory: Rc>, metering: &mut dyn Metering) -> u32 { let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; + let payload_len = fmt_len.saturating_add(arg_len) as usize; + if matches!(metering.on_syscall_data(SYSCALL_LOG, payload_len), MeterResult::Halt) { + panic!("Metering halted SYSCALL_LOG"); + } let borrowed_memory = memory.borrow(); let fmt_slice = match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { Some(s) => s, @@ -450,13 +496,16 @@ impl DefaultSyscallHandler { 0 } - fn sys_call_program(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box) -> u32 { + fn sys_call_program(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box, metering: &mut dyn Metering) -> u32 { let to_ptr = args[0] as usize; let from_ptr = args[1] as usize; let input_ptr = args[2] as usize; let input_len = args[3] as usize; let result_ptr: u32; let page_index: usize; + if matches!(metering.on_call(input_len), MeterResult::Halt) { + panic!("Metering halted SYSCALL_CALL_PROGRAM"); + } { let borrowed_memory = memory.borrow(); let to_slice = match borrowed_memory.mem_slice(to_ptr, to_ptr + 20) { @@ -484,13 +533,20 @@ impl DefaultSyscallHandler { Some(b) => b, None => return 0, }; + if matches!(metering.on_alloc(result_bytes.len()), MeterResult::Halt) { + panic!("Metering halted alloc for call_program result"); + } borrowed_memory.alloc_on_heap(&result_bytes) } } - fn sys_alloc(&mut self, args: [u32; 6], memory: Rc>) -> u32 { + fn sys_alloc(&mut self, args: [u32; 6], memory: Rc>, metering: &mut dyn Metering) -> u32 { let size = args[0] as usize; // A0 register let align = args[1] as usize; // A1 register + + if matches!(metering.on_alloc(size), MeterResult::Halt) { + panic!("Metering halted SYSCALL_ALLOC"); + } if size == 0 { println!("VM Alloc: Invalid size 0"); @@ -537,20 +593,28 @@ impl DefaultSyscallHandler { ptr } - fn sys_dealloc(&mut self, _args: [u32; 6], _memory: Rc>) -> u32 { + fn sys_dealloc(&mut self, args: [u32; 6], _memory: Rc>, metering: &mut dyn Metering) -> u32 { + let size = args[1] as usize; + if matches!(metering.on_alloc(size), MeterResult::Halt) { + panic!("Metering halted SYSCALL_DEALLOC"); + } // Note: This VM uses a simple bump allocator, so we can't actually free memory // In a real VM, you'd implement a proper allocator with free lists // For now, this is a no-op since the memory will be reclaimed when the VM exits 0 } - fn sys_transfer(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box) -> u32 { + fn sys_transfer(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box, metering: &mut dyn Metering) -> u32 { // args: a2=to ptr, a3=value_lo, a4=value_hi let to_ptr = args[1] as usize; let value_lo = args[2] as u64; let value_hi = args[3] as u64; let value = value_lo | (value_hi << 32); + if matches!(metering.on_syscall_data(SYSCALL_TRANSFER, 20), MeterResult::Halt) { + panic!("Metering halted SYSCALL_TRANSFER"); + } + let borrowed = memory.borrow(); let to_slice = borrowed.mem_slice(to_ptr, to_ptr + 20).expect("invalid to ptr"); @@ -560,9 +624,12 @@ impl DefaultSyscallHandler { if host.transfer(to, value) { 0 } else { 1 } } - fn sys_balance(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box) -> u32 { + fn sys_balance(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box, metering: &mut dyn Metering) -> u32 { // args: a1 = address pointer (20 bytes) let addr_ptr = args[0] as usize; + if matches!(metering.on_syscall_data(SYSCALL_BALANCE, 20), MeterResult::Halt) { + panic!("Metering halted SYSCALL_BALANCE"); + } let addr = { let borrowed = memory.borrow(); let addr_slice = borrowed.mem_slice(addr_ptr, addr_ptr + 20).expect("invalid addr ptr"); diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 7c77444..5db6b77 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -6,6 +6,7 @@ use crate::memory_page::{MemoryPage}; use storage::{Storage}; use crate::host_interface::HostInterface; use crate::sys_call::{SyscallHandler, DefaultSyscallHandler}; +use crate::metering::Metering; /// Represents a complete RISC-V virtual machine. /// @@ -69,6 +70,11 @@ impl VM { Self { cpu, memory, storage, host } } + /// Installs a metering implementation on the underlying CPU. + pub fn set_metering(&mut self, metering: Box) { + self.cpu.set_metering(metering); + } + /// Loads program code into memory and sets the starting address. /// /// EDUCATIONAL PURPOSE: This demonstrates how programs are loaded into @@ -262,4 +268,4 @@ impl VM { // EDUCATIONAL: Main execution loop - fetch, decode, execute while self.cpu.step(Rc::clone(&self.memory), Rc::clone(&self.storage), &mut self.host) {} } -} \ No newline at end of file +} diff --git a/crates/vm/tests/allocator_test.rs b/crates/vm/tests/allocator_test.rs index c96907c..b19e0d7 100644 --- a/crates/vm/tests/allocator_test.rs +++ b/crates/vm/tests/allocator_test.rs @@ -1,5 +1,6 @@ use vm::sys_call::{SyscallHandler, DefaultSyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; use vm::{memory_page, host_interface}; +use vm::metering::NoopMeter; use storage::Storage; use std::rc::Rc; use std::cell::RefCell; @@ -10,6 +11,7 @@ fn test_allocator_syscalls() { let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); + let mut meter = NoopMeter::default(); // Test SYSCALL_ALLOC let args = [ @@ -25,6 +27,7 @@ fn test_allocator_syscalls() { storage.clone(), &mut host, &mut regs, + &mut meter, ); println!("✅ SYSCALL_ALLOC returned pointer: 0x{:08x}", result); @@ -43,6 +46,7 @@ fn test_allocator_syscalls() { storage.clone(), &mut host, &mut regs, + &mut meter, ); println!("✅ SYSCALL_DEALLOC returned: {}", dealloc_result); @@ -56,6 +60,7 @@ fn test_multiple_allocations() { let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); let mut regs = [0u32; 32]; + let mut meter = NoopMeter::default(); let mut pointers = Vec::new(); @@ -71,6 +76,7 @@ fn test_multiple_allocations() { storage.clone(), &mut host, &mut regs, + &mut meter, ); println!("✅ Allocation {}: size={}, ptr=0x{:08x}", i, size, ptr); @@ -98,6 +104,7 @@ fn test_alignment_requirements() { let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); let mut regs = [0u32; 32]; + let mut meter = NoopMeter::default(); // Test various alignments let alignments = [1, 2, 4, 8, 16]; @@ -111,6 +118,7 @@ fn test_alignment_requirements() { storage.clone(), &mut host, &mut regs, + &mut meter, ); println!("✅ Alignment test: align={}, ptr=0x{:08x}", align, ptr); @@ -127,6 +135,7 @@ fn test_invalid_alignment() { let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); let mut regs = [0u32; 32]; + let mut meter = NoopMeter::default(); // Test invalid alignments (not powers of 2) let invalid_alignments = [0, 3, 5, 6, 7, 9]; @@ -140,9 +149,10 @@ fn test_invalid_alignment() { storage.clone(), &mut host, &mut regs, + &mut meter, ); println!("✅ Invalid alignment test: align={}, ptr=0x{:08x}", align, ptr); assert_eq!(ptr, 0, "Should return null for invalid alignment {}", align); } -} \ No newline at end of file +} diff --git a/crates/vm/tests/memory_page_offset.rs b/crates/vm/tests/memory_page_offset.rs index 27346e7..5eb8833 100644 --- a/crates/vm/tests/memory_page_offset.rs +++ b/crates/vm/tests/memory_page_offset.rs @@ -1,13 +1,16 @@ use vm::memory_page::MemoryPage; use vm::vm::VM; use vm::sys_call::DefaultSyscallHandler; +use vm::metering::{NoopMeter, MemoryAccessKind}; #[test] fn test_offset_zero_base() { let mem = MemoryPage::new_with_base(1024, 0); + let mut meter = NoopMeter::default(); assert_eq!(mem.offset(0), 0); assert_eq!(mem.offset(100), 100); assert_eq!(mem.offset(1023), 1023); + assert_eq!(mem.load_u32(0, &mut meter, MemoryAccessKind::Load), Some(0)); } #[test] @@ -30,30 +33,33 @@ fn test_offset_below_base_panics() { #[test] fn test_store_and_load_zero_base() { let mem = MemoryPage::new_with_base(1024, 0); - mem.store_u8(10, 0xAB); - assert_eq!(mem.load_byte(10), 0xAB); - mem.store_u16(20, 0xCDEF); - assert_eq!(mem.load_halfword(20), 0xCDEF); - mem.store_u32(30, 0x12345678); - assert_eq!(mem.load_u32(30), 0x12345678); + let mut meter = NoopMeter::default(); + assert!(mem.store_u8(10, 0xAB, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_byte(10, &mut meter, MemoryAccessKind::Load), Some(0xAB)); + assert!(mem.store_u16(20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_halfword(20, &mut meter, MemoryAccessKind::Load), Some(0xCDEF)); + assert!(mem.store_u32(30, 0x12345678, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_u32(30, &mut meter, MemoryAccessKind::Load), Some(0x12345678)); } #[test] fn test_store_and_load_high_base() { let base = 0x80000000; let mem = MemoryPage::new_with_base(1024, base); - mem.store_u8(base + 10, 0xAB); - assert_eq!(mem.load_byte(base + 10), 0xAB); - mem.store_u16(base + 20, 0xCDEF); - assert_eq!(mem.load_halfword(base + 20), 0xCDEF); - mem.store_u32(base + 30, 0x12345678); - assert_eq!(mem.load_u32(base + 30), 0x12345678); + let mut meter = NoopMeter::default(); + assert!(mem.store_u8(base + 10, 0xAB, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_byte(base + 10, &mut meter, MemoryAccessKind::Load), Some(0xAB)); + assert!(mem.store_u16(base + 20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_halfword(base + 20, &mut meter, MemoryAccessKind::Load), Some(0xCDEF)); + assert!(mem.store_u32(base + 30, 0x12345678, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_u32(base + 30, &mut meter, MemoryAccessKind::Load), Some(0x12345678)); } #[test] fn test_store_and_load_at_offset_zero() { let base = 0x80000000; let mem = MemoryPage::new_with_base(1024, base); - mem.store_u8(base, 0xAA); - assert_eq!(mem.load_byte(base), 0xAA); -} \ No newline at end of file + let mut meter = NoopMeter::default(); + assert!(mem.store_u8(base, 0xAA, &mut meter, MemoryAccessKind::Store)); + assert_eq!(mem.load_byte(base, &mut meter, MemoryAccessKind::Load), Some(0xAA)); +} diff --git a/crates/vm/tests/test_syscall_handler.rs b/crates/vm/tests/test_syscall_handler.rs index 90f6ed2..c9d56a6 100644 --- a/crates/vm/tests/test_syscall_handler.rs +++ b/crates/vm/tests/test_syscall_handler.rs @@ -6,6 +6,7 @@ use vm::host_interface::HostInterface; use vm::sys_call::SyscallHandler; use vm::registers::Register; use std::any::Any; +use vm::metering::Metering; /// Map RISC-V test exit codes to test case numbers /// Formula: exit_code = (TESTNUM << 1) | 1 @@ -54,6 +55,7 @@ impl SyscallHandler for TestSyscallHandler { _storage: Rc>, _host: &mut Box, regs: &mut [u32; 32], + _metering: &mut dyn Metering, ) -> (u32, bool) { let mut result = 0; match call_id { @@ -95,4 +97,4 @@ impl SyscallHandler for TestSyscallHandler { fn as_any(&self) -> &dyn Any { self } -} \ No newline at end of file +} From 567fe6ba14a1cc2b19d40ba842f539a2641613db Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 10 Dec 2025 17:36:30 +0200 Subject: [PATCH 02/70] Add gas metering integration and log usage --- crates/avm/README.md | 2 +- crates/avm/src/avm.rs | 29 ++- crates/avm/src/lib.rs | 3 +- crates/avm/src/metering.rs | 266 ++++++++++++++++++++ crates/examples/tests/common/test_runner.rs | 2 + 5 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 crates/avm/src/metering.rs diff --git a/crates/avm/README.md b/crates/avm/README.md index 942bca2..589af92 100644 --- a/crates/avm/README.md +++ b/crates/avm/README.md @@ -65,7 +65,7 @@ All memory access is **local to the context**, preventing accidental overwrites - [x] RISC-V instruction decoding (32-bit and compressed) - [x] Memory-mapped syscall interface - [x] Per-context execution model -- [ ] Gas accounting and metering +- [x] Gas accounting and metering - [ ] Persistent storage via key-based syscalls - [ ] Support for `vm_panic` and return codes - [ ] Debug output and tracing diff --git a/crates/avm/src/avm.rs b/crates/avm/src/avm.rs index 8e4ced2..bee7fbf 100644 --- a/crates/avm/src/avm.rs +++ b/crates/avm/src/avm.rs @@ -1,5 +1,6 @@ use crate::memory_page_manager::MemoryPageManager; use crate::receipt::TransactionReceipt; +use crate::metering::{GasMeter, SharedGasMeter}; use storage::Storage; use vm::vm::VM; use vm::registers::Register; @@ -74,6 +75,11 @@ pub struct AVM { /// can potentially modify this state. pub state: State, + /// Shared gas meter that all contract calls within a transaction use. + /// Nested calls borrow the same counter so they cannot mint gas by + /// re-entering other contracts. + pub gas_meter: Rc>, + pub verbose: bool, // Enable verbose logging for debugging /// Optional writer for verbose output. If None, outputs to console. @@ -86,6 +92,7 @@ impl std::fmt::Debug for AVM { .field("context_stack", &self.context_stack) .field("memory_manager", &self.memory_manager) .field("state", &self.state) + .field("gas_used", &self.gas_meter.borrow().used()) .field("verbose", &self.verbose) .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "Some()")) .finish() @@ -102,7 +109,12 @@ impl AVM { pub fn set_verbose_writer(&mut self, writer: Rc>) { self.verbose_writer = Some(writer); } - + + /// Total gas consumed so far in the current AVM instance. + pub fn gas_used(&self) -> u64 { + self.gas_meter.borrow().used() + } + /// Helper method to log output to either console or the configured writer /// Only logs if verbose is true and self.verbose is enabled fn log(&self, message: &str, verbose: bool) { @@ -140,6 +152,7 @@ impl AVM { context_stack: ContextStack::new(), memory_manager: MemoryPageManager::new(max_pages, page_size), state, + gas_meter: Rc::new(RefCell::new(GasMeter::new())), verbose: false, // Default to no verbose logging verbose_writer: None, // Default to console output } @@ -170,9 +183,8 @@ impl AVM { /// system. This is crucial in blockchain systems where one bad transaction /// shouldn't affect others. /// - /// GAS ACCOUNTING: In real blockchains, each operation costs gas, and - /// transactions have gas limits. This implementation is simplified and - /// doesn't include gas accounting. + /// GAS ACCOUNTING: A shared gas meter (EVM-inspired schedule) is shared + /// across nested calls to prevent gas creation. /// /// RETURN VALUE: Returns a Result indicating success/failure and any error codes pub fn run_tx(&mut self, tx: Transaction) -> TransactionReceipt { @@ -362,6 +374,13 @@ impl AVM { to, hex::encode(&input_data) ), false); + + // Charge the entry call once at the transaction root. Nested contract + // calls initiated via SYSCALL_CALL_PROGRAM are already metered through + // the VM's on_call hook. + if self.context_stack.is_empty() { + let _ = self.gas_meter.borrow_mut().charge_call(input_data.len()); + } // Save address for later use in termination log let to_addr_str = to.to_string(); @@ -393,6 +412,8 @@ impl AVM { // - Enables recursive call_contract logic, since the Box owns the host and doesn't borrow `self` // Without Box, we would need to track lifetimes manually and would hit borrow checker issues. let mut vm: VM = VM::new_with_writer(memory_page, storage.clone(), Box::new(shim), self.verbose_writer.clone()); + let shared_meter = SharedGasMeter::new(Rc::clone(&self.gas_meter)); + vm.set_metering(Box::new(shared_meter)); vm.set_code(0, Config::PROGRAM_START_ADDR, &account.code); vm.cpu.verbose = self.verbose; diff --git a/crates/avm/src/lib.rs b/crates/avm/src/lib.rs index f4361a3..eedeeb9 100644 --- a/crates/avm/src/lib.rs +++ b/crates/avm/src/lib.rs @@ -9,4 +9,5 @@ pub mod global; pub mod execution_context; pub mod router; pub mod host_interface; -pub mod receipt; \ No newline at end of file +pub mod receipt; +pub mod metering; diff --git a/crates/avm/src/metering.rs b/crates/avm/src/metering.rs new file mode 100644 index 0000000..b3cea3d --- /dev/null +++ b/crates/avm/src/metering.rs @@ -0,0 +1,266 @@ +use std::{cell::RefCell, rc::Rc}; + +use vm::instruction::Instruction; +use vm::metering::{MeterResult, Metering, MemoryAccessKind}; +use vm::sys_call::{ + SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, + SYSCALL_LOG, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, +}; + +/// Gas pricing table inspired by the EVM schedule: +/// - Storage operations are expensive (SLOAD/SSTORE style) +/// - External calls and value transfers carry a base "call" charge +/// - Memory copies and logs are charged per byte like calldata/logdata gas +#[derive(Debug, Clone, Copy)] +pub struct GasSchedule { + pub instruction: u64, + pub memory_load_base: u64, + pub memory_store_base: u64, + pub memory_atomic_base: u64, + pub memory_res_load_base: u64, + pub memory_res_store_base: u64, + pub memory_byte_cost: u64, + pub register_read: u64, + pub register_write: u64, + pub pc_update: u64, + pub syscall_base: u64, + pub syscall_storage_get: u64, + pub syscall_storage_set: u64, + pub syscall_log: u64, + pub syscall_call_program: u64, + pub syscall_fire_event: u64, + pub syscall_alloc: u64, + pub syscall_dealloc: u64, + pub syscall_transfer: u64, + pub syscall_balance: u64, + pub call_base: u64, + pub call_data_byte: u64, + pub log_data_byte: u64, + pub storage_key_byte: u64, + pub storage_value_byte: u64, + pub alloc_word: u64, + pub alloc_base: u64, +} + +impl Default for GasSchedule { + fn default() -> Self { + // Costs take cues from Ethereum (London): + // - CALL base ~700, value transfer ~9000, cold storage access ~2100, SSTORE ~20k + // - Calldata/log data charged per byte; memory growth charged per word + Self { + instruction: 1, + memory_load_base: 3, + memory_store_base: 5, + memory_atomic_base: 25, + memory_res_load_base: 12, + memory_res_store_base: 18, + memory_byte_cost: 1, + register_read: 0, + register_write: 0, + pc_update: 0, + syscall_base: 30, + syscall_storage_get: 2100, + syscall_storage_set: 20_000, + syscall_log: 375, + syscall_call_program: 40, // bulk of the cost is charged via call_base/call_data_byte + syscall_fire_event: 375, + syscall_alloc: 15, + syscall_dealloc: 4, + syscall_transfer: 9000, + syscall_balance: 2600, + call_base: 700, + call_data_byte: 4, + log_data_byte: 8, + storage_key_byte: 4, + storage_value_byte: 16, + alloc_word: 3, + alloc_base: 15, + } + } +} + +impl GasSchedule { + fn memory_cost(&self, kind: MemoryAccessKind, bytes: usize) -> u64 { + let per_byte = self.memory_byte_cost.saturating_mul(bytes as u64); + let base = match kind { + MemoryAccessKind::Load => self.memory_load_base, + MemoryAccessKind::Store => self.memory_store_base, + MemoryAccessKind::Atomic => self.memory_atomic_base, + MemoryAccessKind::ReservationLoad => self.memory_res_load_base, + MemoryAccessKind::ReservationStore => self.memory_res_store_base, + }; + base.saturating_add(per_byte) + } + + fn syscall_cost(&self, call_id: u32) -> u64 { + let specific = match call_id { + SYSCALL_STORAGE_GET => self.syscall_storage_get, + SYSCALL_STORAGE_SET => self.syscall_storage_set, + SYSCALL_LOG => self.syscall_log, + SYSCALL_CALL_PROGRAM => self.syscall_call_program, + SYSCALL_FIRE_EVENT => self.syscall_fire_event, + SYSCALL_ALLOC => self.syscall_alloc, + SYSCALL_DEALLOC => self.syscall_dealloc, + SYSCALL_TRANSFER => self.syscall_transfer, + SYSCALL_BALANCE => self.syscall_balance, + _ => 0, + }; + self.syscall_base.saturating_add(specific) + } + + fn syscall_data_cost(&self, call_id: u32, bytes: usize) -> u64 { + let per_byte = match call_id { + SYSCALL_STORAGE_GET => self.storage_key_byte, + SYSCALL_STORAGE_SET => self.storage_value_byte, + SYSCALL_LOG | SYSCALL_FIRE_EVENT => self.log_data_byte, + SYSCALL_TRANSFER | SYSCALL_BALANCE => self.storage_key_byte, + _ => self.call_data_byte, + }; + per_byte.saturating_mul(bytes as u64) + } + + fn alloc_cost(&self, bytes: usize) -> u64 { + let words = ((bytes as u64).saturating_add(31)) / 32; + self.alloc_base + .saturating_add(self.alloc_word.saturating_mul(words.max(1))) + } + + fn call_cost(&self, input_bytes: usize) -> u64 { + self.call_base + .saturating_add(self.call_data_byte.saturating_mul(input_bytes as u64)) + } +} + +/// Core gas accounting state shared across nested contract calls. +#[derive(Debug)] +pub struct GasMeter { + schedule: GasSchedule, + gas_used: u64, +} + +impl GasMeter { + pub fn new() -> Self { + Self { + schedule: GasSchedule::default(), + gas_used: 0, + } + } + + pub fn used(&self) -> u64 { + self.gas_used + } + + pub fn charge_call(&mut self, input_bytes: usize) -> MeterResult { + self.consume(self.schedule.call_cost(input_bytes)) + } + + fn consume(&mut self, amount: u64) -> MeterResult { + if amount == 0 { + return MeterResult::Continue; + } + + self.gas_used = self.gas_used.saturating_add(amount); + MeterResult::Continue + } + + fn charge_instruction(&mut self) -> MeterResult { + self.consume(self.schedule.instruction) + } + + fn charge_memory(&mut self, kind: MemoryAccessKind, bytes: usize) -> MeterResult { + self.consume(self.schedule.memory_cost(kind, bytes)) + } + + fn charge_syscall(&mut self, call_id: u32) -> MeterResult { + self.consume(self.schedule.syscall_cost(call_id)) + } + + fn charge_syscall_data(&mut self, call_id: u32, bytes: usize) -> MeterResult { + self.consume(self.schedule.syscall_data_cost(call_id, bytes)) + } + + fn charge_register_read(&mut self) -> MeterResult { + self.consume(self.schedule.register_read) + } + + fn charge_register_write(&mut self) -> MeterResult { + self.consume(self.schedule.register_write) + } + + fn charge_pc_update(&mut self) -> MeterResult { + self.consume(self.schedule.pc_update) + } + + fn charge_alloc(&mut self, bytes: usize) -> MeterResult { + self.consume(self.schedule.alloc_cost(bytes)) + } +} + +/// Thin adapter that lets the VM/CPU hold a boxed metering implementation +/// while multiple VMs share the same underlying gas counter. +#[derive(Clone)] +pub struct SharedGasMeter { + inner: Rc>, +} + +impl SharedGasMeter { + pub fn new(inner: Rc>) -> Self { + Self { inner } + } + + pub fn used(&self) -> u64 { + self.inner.borrow().used() + } +} + +impl std::fmt::Debug for SharedGasMeter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let borrowed = self.inner.borrow(); + f.debug_struct("SharedGasMeter") + .field("used", &borrowed.used()) + .finish() + } +} + +impl Metering for SharedGasMeter { + fn on_instruction(&mut self, _pc: u32, _instr: &Instruction, _size: u8) -> MeterResult { + self.inner.borrow_mut().charge_instruction() + } + + fn on_memory_access( + &mut self, + kind: MemoryAccessKind, + _addr: usize, + bytes: usize, + ) -> MeterResult { + self.inner.borrow_mut().charge_memory(kind, bytes) + } + + fn on_syscall(&mut self, call_id: u32, _args: &[u32; 6]) -> MeterResult { + self.inner.borrow_mut().charge_syscall(call_id) + } + + fn on_syscall_data(&mut self, call_id: u32, bytes: usize) -> MeterResult { + self.inner.borrow_mut().charge_syscall_data(call_id, bytes) + } + + fn on_register_read(&mut self, _reg: usize) -> MeterResult { + self.inner.borrow_mut().charge_register_read() + } + + fn on_register_write(&mut self, _reg: usize) -> MeterResult { + self.inner.borrow_mut().charge_register_write() + } + + fn on_pc_update(&mut self, _old_pc: u32, _new_pc: u32) -> MeterResult { + self.inner.borrow_mut().charge_pc_update() + } + + fn on_alloc(&mut self, bytes: usize) -> MeterResult { + self.inner.borrow_mut().charge_alloc(bytes) + } + + fn on_call(&mut self, input_bytes: usize) -> MeterResult { + self.inner.borrow_mut().charge_call(input_bytes) + } +} diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index 2a3073c..aae713c 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -148,6 +148,8 @@ impl TestRunner { last_error_code = receipt.result.error_code; last_result = Some(receipt.result.clone()); + writeln!(self.writer.borrow_mut(), "⛽ Gas used so far: {}", avm.gas_used()).unwrap(); + // Write state dump writeln!(self.writer.borrow_mut(), "--- State Dump ---").unwrap(); for (address, account) in &avm.state.accounts { From 8a55028b2d7381539b9ce702aa772c367642f0e2 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 12 Dec 2025 11:38:18 +0200 Subject: [PATCH 03/70] Add aOS crate scaffold --- Cargo.lock | 4 ++ Cargo.toml | 1 + crates/aos/Cargo.toml | 7 +++ crates/aos/README.md | 58 ++++++++++++++++++ crates/aos/src/lib.rs | 133 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 crates/aos/Cargo.toml create mode 100644 crates/aos/README.md create mode 100644 crates/aos/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3928bc8..cebf0f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,10 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aos" +version = "0.1.0" + [[package]] name = "avm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 782d4be..bee76cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/avm", "crates/compiler", "crates/examples", + "crates/aos", "crates/program", "crates/state", "crates/storage", diff --git a/crates/aos/Cargo.toml b/crates/aos/Cargo.toml new file mode 100644 index 0000000..10a4d41 --- /dev/null +++ b/crates/aos/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "aos" +version = "0.1.0" +edition = "2024" +readme = "README.md" + +[dependencies] diff --git a/crates/aos/README.md b/crates/aos/README.md new file mode 100644 index 0000000..64df4ec --- /dev/null +++ b/crates/aos/README.md @@ -0,0 +1,58 @@ +# aOS (Alon's OS) + +Alon's OS (aOS) is a minimal operating system purpose-built for deterministic, blockchain-style program execution. It replaces the current `avm` crate with a layered OS: a bootloader for trust establishment, a kernel that orchestrates stateful execution, and `liba`, the standard library that application programs link against. + +## Goals +- Deterministic, replayable execution for consensus environments +- Small, auditable surface area with a clear chain of trust from ROM to apps +- Opinionated syscall and runtime model tailored for blockchain state transitions +- First-class support for smart-contract style programs through `liba` +- Ergonomic developer experience while keeping kernel/runtime minimal + +## Layered Architecture +1. **Bootloader**: First-stage loader that verifies the kernel and `liba` images, measures them, and passes a concise boot manifest to the kernel. Runs in a restricted environment with no dynamic allocation. +2. **Kernel**: Manages memory layout, page tables, capabilities, and the syscall surface. Provides deterministic scheduling and ties block context (slot, leader, parent state root) to every execution. +3. **Execution Runtime**: The blockchain-aware executor that will supersede `avm`. It coordinates transaction/block execution, drives the VM, and emits receipts and event logs. +4. **liba (Standard Library)**: Successor of the `program` crate, offering safe wrappers over syscalls (storage, logs, crypto, messaging), ABI helpers, and contract-to-contract call utilities. +5. **Tooling**: Compiler and host utilities reused from the existing workspace to build and package aOS images and applications. + +## Component Details +### Bootloader +- Validates and measures the kernel and `liba` artifacts before execution. +- Builds a `BootInfo` payload (memory map, entry points, config) handed to the kernel. +- Provides a minimal diagnostic console for early-boot errors. + +### Kernel +- Sets up paging and isolates execution contexts (per-transaction or per-program). +- Implements a deterministic scheduler and resource accounting suited for block production. +- Exposes a constrained syscall table: storage access, logging/events, crypto primitives, time/slot metadata, inter-program calls. +- Tracks state roots and receipts to keep execution verifiable. + +### Execution Runtime +- Drives the VM for each transaction, wiring block context into the kernel-provided syscalls. +- Applies state transitions via the `state` and `storage` crates and emits receipts for verification. +- Provides hooks for precompiles and deterministic host functions. + +### liba (Application Standard Library) +- Derived from the existing `program` crate and tailored to aOS. +- Offers ABI types, context helpers, and safe wrappers around syscalls exposed by the kernel. +- Ships default modules for storage, logging/events, cross-program calls, and crypto utilities. + +## Program Lifecycle (Happy Path) +1. Bootloader measures kernel + `liba`, builds `BootInfo`, and jumps to the kernel. +2. Kernel sets up memory and installs syscall table based on the boot manifest. +3. For each block, the runtime constructs execution contexts with block metadata and state snapshots. +4. Transactions enter the VM; `liba` mediates syscalls to kernel services. +5. State and receipts are persisted; the resulting state root/receipts are exposed to consensus. + +## Relationship to Existing Workspace +- `aos` replaces the `avm` crate as the orchestrator/runtime. +- `program` is internalized as `liba` inside this crate (module structure will mirror the current APIs). +- `vm`, `state`, `storage`, `types`, and `compiler` remain the core building blocks for CPU execution, state transitions, persistence, shared types, and toolchain support. + +## Roadmap (Initial Steps) +- Define kernel <-> runtime <-> `liba` interfaces and shared types. +- Port `program` into `liba` and expose it as the supported app-facing API. +- Migrate `avm` responsibilities into the aOS runtime and deprecate `avm`. +- Add a boot manifest format and minimal bootloader stubs to validate and launch the kernel. +- Document syscall semantics and determinism guarantees for contract authors. diff --git a/crates/aos/src/lib.rs b/crates/aos/src/lib.rs new file mode 100644 index 0000000..2c5f30b --- /dev/null +++ b/crates/aos/src/lib.rs @@ -0,0 +1,133 @@ +#![forbid(unsafe_code)] +//! Alon's OS (aOS): a deterministic, blockchain-first operating system. +//! +//! This crate hosts the top-level types and module layout for aOS. It will grow to +//! replace the current `avm` orchestrator while internalizing the `program` crate as +//! the `liba` standard library for applications. + +/// Marker type for the OS surface. It will eventually own the boot/kernel/runtime +/// wiring that `avm` performs today. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Aos; + +/// Bootloader-focused types used to validate and hand off control to the kernel. +pub mod bootloader { + /// Build-time configuration the bootloader uses while measuring the kernel and + /// standard library images. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct BootConfig { + /// Whether the bootloader should emit early console logs. + pub debug_console: bool, + /// Whether the bootloader may proceed with unsigned developer images. + pub allow_developer_images: bool, + } + + impl Default for BootConfig { + fn default() -> Self { + Self { + debug_console: true, + allow_developer_images: false, + } + } + } + + /// Minimal manifest created by the bootloader and consumed by the kernel. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct BootInfo<'a> { + /// Verified kernel image payload. + pub kernel_image: &'a [u8], + /// Verified `liba` (standard library) image payload. + pub liba_image: &'a [u8], + /// Boot-time options that tailor kernel behavior. + pub config: BootConfig, + } + + impl<'a> BootInfo<'a> { + /// Creates a new manifest describing the verified runtime artifacts. + pub fn new(kernel_image: &'a [u8], liba_image: &'a [u8], config: BootConfig) -> Self { + Self { + kernel_image, + liba_image, + config, + } + } + } +} + +/// Kernel-facing structures for deterministic, capability-scoped execution. +pub mod kernel { + /// Configuration that controls which services the kernel exposes to runtimes. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct KernelConfig { + /// Whether to emit receipts and event logs. + pub receipts_enabled: bool, + /// Whether to allow cross-program calls. + pub cross_program_calls_enabled: bool, + /// Whether to allow host crypto/syscall helpers. + pub host_functions_enabled: bool, + } + + impl Default for KernelConfig { + fn default() -> Self { + Self { + receipts_enabled: true, + cross_program_calls_enabled: true, + host_functions_enabled: true, + } + } + } + + /// Placeholder for the kernel instance that will own scheduling and capability setup. + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] + pub struct Kernel; +} + +/// Runtime-level types that coordinate block execution and VM orchestration. +pub mod runtime { + /// Execution context provided to each transaction/program. + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ExecutionContext { + /// Height or slot of the block being executed. + pub height: u64, + /// Hash of the parent state root for reproducibility. + pub parent_state_root: [u8; 32], + /// Index of the transaction inside the block. + pub tx_index: u32, + } + + impl ExecutionContext { + /// Builds a new execution context for a transaction. + pub fn new(height: u64, parent_state_root: [u8; 32], tx_index: u32) -> Self { + Self { + height, + parent_state_root, + tx_index, + } + } + } + + /// Placeholder for the runtime that will supersede the `avm` orchestrator. + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] + pub struct Runtime; +} + +/// `liba` is the application-facing standard library derived from the existing +/// `program` crate. +pub mod liba { + /// Enumeration of syscalls that applications may perform through `liba`. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Syscall { + /// Persist or retrieve data from the underlying storage. + Storage, + /// Emit events/logs that become part of transaction receipts. + Log, + /// Perform cryptographic helper operations. + Crypto, + /// Invoke another program within the same block execution. + CrossProgramCall, + } + + /// Minimal handle for the standard library surface exposed to applications. + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] + pub struct Liba; +} From 2f090a647da262ffeb3d7d13ab13c333e6e370f7 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 12 Dec 2025 22:57:00 +0200 Subject: [PATCH 04/70] Organize memory modules --- crates/avm/src/avm.rs | 76 +- crates/avm/src/execution_context.rs | 41 +- crates/avm/src/global.rs | 4 +- crates/avm/src/host_interface.rs | 24 +- crates/avm/src/lib.rs | 10 +- .../{vm/src => avm/src/memory}/memory_page.rs | 156 ++- .../src/{ => memory}/memory_page_manager.rs | 32 +- crates/avm/src/memory/mod.rs | 5 + crates/avm/src/metering.rs | 2 +- crates/avm/src/receipt.rs | 65 +- crates/avm/src/transaction.rs | 2 +- crates/{vm => avm}/tests/allocator_test.rs | 74 +- .../{vm => avm}/tests/memory_page_offset.rs | 41 +- crates/{vm => avm}/tests/spec_runner.rs | 180 +-- .../{vm => avm}/tests/test_syscall_handler.rs | 42 +- crates/vm/src/cpu.rs | 107 +- crates/vm/src/exe.rs | 1089 +++++++++++++---- crates/vm/src/lib.rs | 14 +- crates/vm/src/memory.rs | 26 + crates/vm/src/sys_call.rs | 378 ++++-- crates/vm/src/vm.rs | 151 ++- 21 files changed, 1764 insertions(+), 755 deletions(-) rename crates/{vm/src => avm/src/memory}/memory_page.rs (63%) rename crates/avm/src/{ => memory}/memory_page_manager.rs (77%) create mode 100644 crates/avm/src/memory/mod.rs rename crates/{vm => avm}/tests/allocator_test.rs (64%) rename crates/{vm => avm}/tests/memory_page_offset.rs (68%) rename crates/{vm => avm}/tests/spec_runner.rs (63%) rename crates/{vm => avm}/tests/test_syscall_handler.rs (78%) create mode 100644 crates/vm/src/memory.rs diff --git a/crates/avm/src/avm.rs b/crates/avm/src/avm.rs index bee7fbf..967a981 100644 --- a/crates/avm/src/avm.rs +++ b/crates/avm/src/avm.rs @@ -1,20 +1,23 @@ -use crate::memory_page_manager::MemoryPageManager; -use crate::receipt::TransactionReceipt; -use crate::metering::{GasMeter, SharedGasMeter}; -use storage::Storage; -use vm::vm::VM; -use vm::registers::Register; -use state::{State, Account}; -use crate::transaction::{TransactionType, Transaction}; +use crate::execution_context::{ContextStack, ExecutionContext}; use crate::global::Config; -use crate::execution_context::{ExecutionContext, ContextStack}; use crate::host_interface::HostShim; -use types::address::Address; -use types::result::Result; -use std::{panic::{catch_unwind, AssertUnwindSafe}, usize}; -use std::rc::Rc; +use crate::memory::MemoryPageManager; +use crate::metering::{GasMeter, SharedGasMeter}; +use crate::receipt::TransactionReceipt; +use crate::transaction::{Transaction, TransactionType}; use core::cell::RefCell; use core::fmt::Write; +use state::{Account, State}; +use std::rc::Rc; +use std::{ + panic::{AssertUnwindSafe, catch_unwind}, + usize, +}; +use storage::Storage; +use types::address::Address; +use types::result::Result; +use vm::registers::Register; +use vm::vm::VM; /// Application Virtual Machine (AVM) - the main orchestrator for smart contract execution. /// @@ -211,10 +214,14 @@ impl AVM { TransactionType::ProgramCall => { // EDUCATIONAL: Execute an existing smart contract // First verify the destination is actually a contract - assert!(self.state.is_contract(tx.to), "destination address is not a contract"); + assert!( + self.state.is_contract(tx.to), + "destination address is not a contract" + ); // EDUCATIONAL: Call the contract and extract the result - let (result_ptr, context_index) = self.call_contract(tx.from, tx.to, tx.data.clone()); + let (result_ptr, context_index) = + self.call_contract(tx.from, tx.to, tx.data.clone()); // verify context stack is empty if !self.context_stack.is_empty() { @@ -262,9 +269,12 @@ impl AVM { /// to prevent reading invalid memory. fn extract_result(&self, _result_ptr: u32, context_index: usize) -> Result { // EDUCATIONAL: Get the memory page where the result was stored - let ee = self.context_stack.get(context_index).expect("missing execution context"); + let ee = self + .context_stack + .get(context_index) + .expect("missing execution context"); let vm = ee.vm.borrow(); - let page = vm.memory.borrow(); + let page = vm.memory.as_ref(); // EDUCATIONAL: Use the memory page's offset calculation to get the correct memory location let start = page.offset(Config::RESULT_ADDR as usize); // Use memory page offset @@ -424,12 +434,21 @@ impl AVM { // add new context execution let context_index = self.context_stack.push(from, to, input_data, vm); - let context = self.context_stack.current_mut().expect("missing execution context"); + let context = self + .context_stack + .current_mut() + .expect("missing execution context"); // EDUCATIONAL: Set up function parameters in registers // This follows the RISC-V calling convention - let _address_ptr = context.vm.borrow_mut().set_reg_to_data(Register::A0, to.0.as_ref()); // Contract address - let _pubkey_ptr = context.vm.borrow_mut().set_reg_to_data(Register::A1, from.0.as_ref()); // Caller address + let _address_ptr = context + .vm + .borrow_mut() + .set_reg_to_data(Register::A0, to.0.as_ref()); // Contract address + let _pubkey_ptr = context + .vm + .borrow_mut() + .set_reg_to_data(Register::A1, from.0.as_ref()); // Caller address // EDUCATIONAL: Validate input size to prevent resource exhaustion let input_len = context.input_data.len(); @@ -442,8 +461,14 @@ impl AVM { } // EDUCATIONAL: Set up input data (no result pointer needed) - let _input_ptr = context.vm.borrow_mut().set_reg_to_data(Register::A2, &context.input_data); // Input data - context.vm.borrow_mut().set_reg_u32(Register::A3, input_len as u32); // Input length + let _input_ptr = context + .vm + .borrow_mut() + .set_reg_to_data(Register::A2, &context.input_data); // Input data + context + .vm + .borrow_mut() + .set_reg_u32(Register::A3, input_len as u32); // Input length // EDUCATIONAL: Run the VM safely with panic handling let result = catch_unwind(AssertUnwindSafe(|| { @@ -463,9 +488,12 @@ impl AVM { // EDUCATIONAL: set context execution done context.exe_done = true; - + // Log execution termination for binary comparison tracking (after all borrows are done) - self.log(&format!("Execution terminated for address {}", to_addr_str), false); + self.log( + &format!("Execution terminated for address {}", to_addr_str), + false, + ); (Config::RESULT_ADDR, context_index) // Fixed result address } diff --git a/crates/avm/src/execution_context.rs b/crates/avm/src/execution_context.rs index ea262b8..2327b72 100644 --- a/crates/avm/src/execution_context.rs +++ b/crates/avm/src/execution_context.rs @@ -1,6 +1,6 @@ -use types::address::Address; -use std::rc::Rc; use std::cell::RefCell; +use std::rc::Rc; +use types::address::Address; use vm::vm::VM; /// Represents a single execution context during contract calls. @@ -13,32 +13,27 @@ pub struct ExecutionContext { pub to: Address, // Data passed to the contract call - pub input_data: Rc>, + pub input_data: Rc>, // Memory page pub vm: Rc>, - pub events: Vec>, + pub events: Vec>, // is exe_done marks context as executed pub exe_done: bool, } impl ExecutionContext { - pub fn new( - from: Address, - to: Address, - input_data: Vec, - vm: VM, - ) -> Self { - Self { + pub fn new(from: Address, to: Address, input_data: Vec, vm: VM) -> Self { + Self { from, to, - input_data: Rc::new(input_data), - vm: Rc::new(RefCell::new(vm)), + input_data: Rc::new(input_data), + vm: Rc::new(RefCell::new(vm)), events: Vec::new(), exe_done: false, - } + } } } @@ -58,15 +53,14 @@ impl ContextStack { /// returns index of the new execution context pub fn push(&mut self, from: Address, to: Address, input_data: Vec, vm: VM) -> usize { let index = self.stack.len(); - self.stack.push( - ExecutionContext { - from, - to, - input_data:Rc::new(input_data), - vm:Rc::new(RefCell::new(vm)), - events: Vec::new(), - exe_done: false, - }); + self.stack.push(ExecutionContext { + from, + to, + input_data: Rc::new(input_data), + vm: Rc::new(RefCell::new(vm)), + events: Vec::new(), + exe_done: false, + }); index } @@ -93,7 +87,6 @@ impl ContextStack { self.stack.last_mut() } - pub fn iter(&self) -> impl Iterator { self.stack.iter() } diff --git a/crates/avm/src/global.rs b/crates/avm/src/global.rs index fb52c51..e39b61b 100644 --- a/crates/avm/src/global.rs +++ b/crates/avm/src/global.rs @@ -2,8 +2,8 @@ pub struct Config; impl Config { pub const MAX_INPUT_LEN: usize = 1024; - pub const CODE_SIZE_LIMIT: usize = 0x30000; // 192KB headroom for non-compressed RV32IM binaries - pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; // 8KB for read-only data + pub const CODE_SIZE_LIMIT: usize = 0x30000; // 192KB headroom for non-compressed RV32IM binaries + pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; // 8KB for read-only data pub const HEAP_START_ADDR: usize = Self::CODE_SIZE_LIMIT + Self::RO_DATA_SIZE_LIMIT + 0x100; pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; diff --git a/crates/avm/src/host_interface.rs b/crates/avm/src/host_interface.rs index 6a487a3..32f94f8 100644 --- a/crates/avm/src/host_interface.rs +++ b/crates/avm/src/host_interface.rs @@ -1,6 +1,6 @@ -use vm::host_interface::HostInterface; -use types::address::Address; use crate::avm::AVM; +use types::address::Address; +use vm::host_interface::HostInterface; // HostShim is a lightweight adapter that allows a VM to call back into the AVM. // It implements the HostInterface trait and holds a raw pointer to the AVM. @@ -43,7 +43,11 @@ impl<'a> HostInterface for HostShim { unsafe { // SAFETY: self.avm_ptr must point to a valid AVM that has access to the callee's memory let avm = &mut *self.avm_ptr; - avm.context_stack.current_mut().expect("must have current context").events.push(event.clone()); + avm.context_stack + .current_mut() + .expect("must have current context") + .events + .push(event.clone()); let hex_string: String = event .iter() @@ -55,14 +59,22 @@ impl<'a> HostInterface for HostShim { } } - fn read_memory_page(&mut self, page_index: usize, guest_ptr: u32, len: usize) -> Option> { + fn read_memory_page( + &mut self, + page_index: usize, + guest_ptr: u32, + len: usize, + ) -> Option> { unsafe { // SAFETY: self.avm_ptr must point to a valid AVM that has access to the callee's memory let avm = &*self.avm_ptr; - let ee = avm.context_stack.get(page_index).expect("missing execution context"); + let ee = avm + .context_stack + .get(page_index) + .expect("missing execution context"); let vm = ee.vm.borrow(); - let page_ref = vm.memory.borrow(); + let page_ref = vm.memory.as_ref(); // Assume the callee's memory manager is accessible here let mem = page_ref.mem(); diff --git a/crates/avm/src/lib.rs b/crates/avm/src/lib.rs index eedeeb9..dd6c9b3 100644 --- a/crates/avm/src/lib.rs +++ b/crates/avm/src/lib.rs @@ -3,11 +3,11 @@ pub extern crate hex; // exports pub mod avm; -pub mod transaction; -pub mod memory_page_manager; -pub mod global; pub mod execution_context; -pub mod router; +pub mod global; pub mod host_interface; -pub mod receipt; +pub mod memory; pub mod metering; +pub mod receipt; +pub mod router; +pub mod transaction; diff --git a/crates/vm/src/memory_page.rs b/crates/avm/src/memory/memory_page.rs similarity index 63% rename from crates/vm/src/memory_page.rs rename to crates/avm/src/memory/memory_page.rs index f3756d9..d453bad 100644 --- a/crates/vm/src/memory_page.rs +++ b/crates/avm/src/memory/memory_page.rs @@ -1,7 +1,8 @@ -use std::rc::Rc; -use std::cell::{RefCell, Cell}; +use std::cell::{Cell, Ref, RefCell}; use std::convert::TryInto; -use crate::metering::{Metering, MeterResult, MemoryAccessKind}; +use std::rc::Rc; +use vm::memory::{Memory, HEAP_PTR_OFFSET}; +use vm::metering::{MeterResult, Metering, MemoryAccessKind}; #[derive(Debug, Clone)] pub struct MemoryPage { @@ -10,8 +11,6 @@ pub struct MemoryPage { pub base_address: usize, // base address for guest memory mapping } -pub const HEAP_PTR_OFFSET: u32 = 0x100; - impl MemoryPage { pub fn new_with_base(memory_size: usize, base_address: usize) -> Self { Self { @@ -20,11 +19,12 @@ impl MemoryPage { base_address, } } + pub fn new(memory_size: usize) -> Self { Self::new_with_base(memory_size, 0) } - pub fn mem(&self) -> std::cell::Ref> { + pub fn mem(&self) -> Ref> { self.mem.borrow() } @@ -34,7 +34,8 @@ impl MemoryPage { } pub fn offset(&self, addr: usize) -> usize { - addr.checked_sub(self.base_address).expect("Address below base_address") + addr.checked_sub(self.base_address) + .expect("Address below base_address") } fn meter_access( @@ -43,7 +44,10 @@ impl MemoryPage { addr: usize, bytes: usize, ) -> bool { - matches!(metering.on_memory_access(kind, addr, bytes), MeterResult::Continue) + matches!( + metering.on_memory_access(kind, addr, bytes), + MeterResult::Continue + ) } pub fn store_u16( @@ -64,7 +68,7 @@ impl MemoryPage { mem[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); true } - + pub fn store_u32( &self, addr: usize, @@ -117,7 +121,9 @@ impl MemoryPage { if offset + 4 > mem.len() { panic!("load u32 out of bounds: addr = 0x{:08x}", addr); } - Some(u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap())) + Some(u32::from_le_bytes( + mem[offset..offset + 4].try_into().unwrap(), + )) } pub fn load_byte( @@ -145,7 +151,9 @@ impl MemoryPage { } let offset = self.offset(addr); let mem = self.mem.borrow(); - Some(u16::from_le_bytes(mem[offset..offset + 2].try_into().unwrap())) + Some(u16::from_le_bytes( + mem[offset..offset + 2].try_into().unwrap(), + )) } pub fn load_word( @@ -159,7 +167,9 @@ impl MemoryPage { } let offset = self.offset(addr); let mem = self.mem.borrow(); - Some(u32::from_le_bytes(mem[offset..offset + 4].try_into().unwrap())) + Some(u32::from_le_bytes( + mem[offset..offset + 4].try_into().unwrap(), + )) } pub fn mem_slice(&self, start: usize, end: usize) -> Option> { @@ -172,14 +182,15 @@ impl MemoryPage { Some(std::cell::Ref::map(mem_ref, move |v| &v[start_offset..end_offset])) } - pub fn write_code(&mut self, start_addr: usize, code: &[u8]) { + pub fn write_code(&self, start_addr: usize, code: &[u8]) { let start_offset = self.offset(start_addr); let mut mem = self.mem.borrow_mut(); let end = start_offset + code.len(); mem[start_offset..end].copy_from_slice(code); // set heap pointer - self.next_heap = Cell::new(start_offset as u32 + code.len() as u32 + HEAP_PTR_OFFSET); + self.next_heap + .set(start_offset as u32 + code.len() as u32 + HEAP_PTR_OFFSET); } pub fn alloc_on_heap(&self, data: &[u8]) -> u32 { @@ -188,15 +199,20 @@ impl MemoryPage { // Align to 4 bytes (or 8 if you're storing u64s) let align = 8; addr = (addr + (align - 1)) & !(align - 1); - + let end = addr + data.len() as u32; - assert!(end as usize <= self.size(), "Out of memory: trying to allocate {} bytes, but only {} bytes available", data.len(), self.size() - addr as usize); + assert!( + end as usize <= self.size(), + "Out of memory: trying to allocate {} bytes, but only {} bytes available", + data.len(), + self.size() - addr as usize + ); self.mem.borrow_mut()[addr as usize..end as usize].copy_from_slice(data); self.next_heap.set(end); addr - } + } pub fn stack_top(&self) -> u32 { self.size() as u32 @@ -209,10 +225,114 @@ impl Default for MemoryPage { } } +impl Memory for MemoryPage { + fn mem(&self) -> Ref> { + MemoryPage::mem(self) + } + + fn mem_slice(&self, start: usize, end: usize) -> Option> { + MemoryPage::mem_slice(self, start, end) + } + + fn store_u16( + &self, + addr: usize, + val: u16, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + MemoryPage::store_u16(self, addr, val, metering, kind) + } + + fn store_u32( + &self, + addr: usize, + val: u32, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + MemoryPage::store_u32(self, addr, val, metering, kind) + } + + fn store_u8( + &self, + addr: usize, + val: u8, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + MemoryPage::store_u8(self, addr, val, metering, kind) + } + + fn load_u32( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_u32(self, addr, metering, kind) + } + + fn load_byte( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_byte(self, addr, metering, kind) + } + + fn load_halfword( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_halfword(self, addr, metering, kind) + } + + fn load_word( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_word(self, addr, metering, kind) + } + + fn write_code(&self, start_addr: usize, code: &[u8]) { + MemoryPage::write_code(self, start_addr, code) + } + + fn alloc_on_heap(&self, data: &[u8]) -> u32 { + MemoryPage::alloc_on_heap(self, data) + } + + fn stack_top(&self) -> u32 { + MemoryPage::stack_top(self) + } + + fn size(&self) -> usize { + MemoryPage::size(self) + } + + fn offset(&self, addr: usize) -> usize { + MemoryPage::offset(self, addr) + } + + fn next_heap(&self) -> u32 { + self.next_heap.get() + } + + fn set_next_heap(&self, next: u32) { + self.next_heap.set(next); + } +} + #[cfg(test)] mod tests { use super::*; - use crate::metering::NoopMeter; + use vm::metering::NoopMeter; #[test] fn test_offset_zero_base() { diff --git a/crates/avm/src/memory_page_manager.rs b/crates/avm/src/memory/memory_page_manager.rs similarity index 77% rename from crates/avm/src/memory_page_manager.rs rename to crates/avm/src/memory/memory_page_manager.rs index 07f0f7b..77bbc9f 100644 --- a/crates/avm/src/memory_page_manager.rs +++ b/crates/avm/src/memory/memory_page_manager.rs @@ -1,11 +1,12 @@ -use vm::memory_page::MemoryPage; -use std::{cell::RefCell, rc::Rc}; +use std::rc::Rc; +use crate::memory::MemoryPage; +use vm::memory::SharedMemory; #[derive(Debug)] pub struct MemoryPageManager { pub page_size: usize, max_pages: usize, - pages: Vec>>, + pages: Vec, } impl MemoryPageManager { @@ -21,12 +22,15 @@ impl MemoryPageManager { } /// Creates and owns a new page. Returns a mutable reference to it. - pub fn new_page(&mut self) -> Rc> { + pub fn new_page(&mut self) -> SharedMemory { if self.pages.len() >= self.max_pages { - panic!("Out of memory: maximum page count ({}) reached", self.max_pages); + panic!( + "Out of memory: maximum page count ({}) reached", + self.max_pages + ); } - let page = Rc::new(RefCell::new(MemoryPage::new(self.page_size))); + let page: SharedMemory = Rc::new(MemoryPage::new(self.page_size)); self.pages.push(Rc::clone(&page)); return page; } @@ -41,8 +45,7 @@ impl MemoryPageManager { for (i, page_rc) in self.pages.iter().enumerate() { println!("\n=== Page {} ===", i); - let page = page_rc.borrow(); - let mem = page.mem(); + let mem = page_rc.mem(); for (j, chunk) in mem.chunks(16).enumerate() { print!("0x{:04x}: ", j * 16); @@ -71,20 +74,17 @@ impl MemoryPageManager { println!("|"); } } -} - - + } - pub fn get_page(&self, index: usize) -> Option>> { + pub fn get_page(&self, index: usize) -> Option { self.pages.get(index).cloned() // ✅ clone the Rc (increases refcount) } - pub fn first_page(&self) -> Option>> { + pub fn first_page(&self) -> Option { self.pages.first().cloned() // ✅ clone the Rc (increases refcount) } - pub fn top_page(&self) -> Option>> { + pub fn top_page(&self) -> Option { self.pages.last().cloned() // ✅ clone the Rc (increases refcount) - } - + } } diff --git a/crates/avm/src/memory/mod.rs b/crates/avm/src/memory/mod.rs new file mode 100644 index 0000000..6da46d1 --- /dev/null +++ b/crates/avm/src/memory/mod.rs @@ -0,0 +1,5 @@ +pub mod memory_page; +pub mod memory_page_manager; + +pub use memory_page::MemoryPage; +pub use memory_page_manager::MemoryPageManager; diff --git a/crates/avm/src/metering.rs b/crates/avm/src/metering.rs index b3cea3d..d267c8e 100644 --- a/crates/avm/src/metering.rs +++ b/crates/avm/src/metering.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, rc::Rc}; use vm::instruction::Instruction; -use vm::metering::{MeterResult, Metering, MemoryAccessKind}; +use vm::metering::{MemoryAccessKind, MeterResult, Metering}; use vm::sys_call::{ SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, diff --git a/crates/avm/src/receipt.rs b/crates/avm/src/receipt.rs index dd40942..9ab4655 100644 --- a/crates/avm/src/receipt.rs +++ b/crates/avm/src/receipt.rs @@ -1,5 +1,5 @@ -use types::{Result}; use crate::transaction::Transaction; +use types::Result; /// Represents the result of a transaction execution. #[derive(Debug, Clone)] @@ -12,7 +12,6 @@ pub struct TransactionReceipt { // /// Gas used by this transaction alone. // pub gas_used: u64, - pub result: Result, /// List of log entries generated during execution. @@ -36,7 +35,7 @@ impl TransactionReceipt { self.events.push(event); self } - + /// Optionally add multiple events at once. pub fn set_events(mut self, events: Vec>) -> Self { self.events = events; @@ -55,7 +54,11 @@ impl fmt::Display for TransactionReceipt { writeln!(f, "Events:")?; for (i, event) in self.events.iter().enumerate() { - let hex = event.iter().map(|b| format!("{:02x}", b)).collect::>().join(" "); + let hex = event + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(" "); writeln!(f, " [{}] {}", i, hex)?; } @@ -78,7 +81,11 @@ impl TransactionReceipt { let _ = writeln!(writer); } - pub fn pretty_print_event(event: &[u8], abi_registry: &Vec, writer: &mut dyn fmt::Write) { + pub fn pretty_print_event( + event: &[u8], + abi_registry: &Vec, + writer: &mut dyn fmt::Write, + ) { if event.len() < 32 { let _ = writeln!(writer, "Invalid event: too short"); return; @@ -100,7 +107,11 @@ impl TransactionReceipt { match param.kind { ParamType::Address => { if offset + 20 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let bytes: &[u8] = &data[offset..offset + 20]; @@ -109,7 +120,11 @@ impl TransactionReceipt { } ParamType::Uint(256) => { if offset + 32 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let bytes = &data[offset..offset + 32]; @@ -118,7 +133,11 @@ impl TransactionReceipt { } ParamType::Uint(128) => { if offset + 16 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let bytes = &data[offset..offset + 16]; @@ -128,7 +147,11 @@ impl TransactionReceipt { } ParamType::Uint(64) => { if offset + 8 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let bytes = &data[offset..offset + 8]; @@ -138,7 +161,11 @@ impl TransactionReceipt { } ParamType::Uint(32) => { if offset + 4 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let bytes = &data[offset..offset + 4]; @@ -148,7 +175,11 @@ impl TransactionReceipt { } ParamType::Bool => { if offset + 1 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let b = data[offset]; @@ -157,13 +188,21 @@ impl TransactionReceipt { } ParamType::Bytes => { if offset + 1 > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let len = data[offset] as usize; offset += 1; if offset + len > data.len() { - let _ = writeln!(writer, " {}: ", param.name); + let _ = writeln!( + writer, + " {}: ", + param.name + ); break; } let bytes = &data[offset..offset + len]; diff --git a/crates/avm/src/transaction.rs b/crates/avm/src/transaction.rs index 932d0214..182c428 100644 --- a/crates/avm/src/transaction.rs +++ b/crates/avm/src/transaction.rs @@ -44,4 +44,4 @@ impl TransactionBundle { pub fn is_empty(&self) -> bool { self.transactions.is_empty() } -} \ No newline at end of file +} diff --git a/crates/vm/tests/allocator_test.rs b/crates/avm/tests/allocator_test.rs similarity index 64% rename from crates/vm/tests/allocator_test.rs rename to crates/avm/tests/allocator_test.rs index b19e0d7..308b766 100644 --- a/crates/vm/tests/allocator_test.rs +++ b/crates/avm/tests/allocator_test.rs @@ -1,24 +1,22 @@ -use vm::sys_call::{SyscallHandler, DefaultSyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; -use vm::{memory_page, host_interface}; -use vm::metering::NoopMeter; -use storage::Storage; -use std::rc::Rc; +use avm::memory::MemoryPage; use std::cell::RefCell; +use std::rc::Rc; +use storage::Storage; +use vm::host_interface; +use vm::metering::NoopMeter; +use vm::memory::SharedMemory; +use vm::sys_call::{DefaultSyscallHandler, SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); + let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); let mut meter = NoopMeter::default(); // Test SYSCALL_ALLOC - let args = [ - 1024, // size - 8, // alignment - 0, 0, 0, 0 - ]; + let args = [1024, 8, 0, 0, 0, 0]; let mut regs = [0u32; 32]; let (result, _) = syscall_handler.handle_syscall( SYSCALL_ALLOC, @@ -30,15 +28,10 @@ fn test_allocator_syscalls() { &mut meter, ); - println!("✅ SYSCALL_ALLOC returned pointer: 0x{:08x}", result); - assert_ne!(result, 0); // Should return valid pointer + assert_ne!(result, 0); // Test SYSCALL_DEALLOC (no-op but should not crash) - let dealloc_args = [ - result, // pointer to deallocate - 1024, // size - 0, 0, 0, 0 - ]; + let dealloc_args = [result, 1024, 0, 0, 0, 0]; let (dealloc_result, _) = syscall_handler.handle_syscall( SYSCALL_DEALLOC, dealloc_args, @@ -49,13 +42,12 @@ fn test_allocator_syscalls() { &mut meter, ); - println!("✅ SYSCALL_DEALLOC returned: {}", dealloc_result); - assert_eq!(dealloc_result, 0); // Should return 0 (success) + assert_eq!(dealloc_result, 0); } -#[test] +#[test] fn test_multiple_allocations() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); + let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); @@ -66,9 +58,9 @@ fn test_multiple_allocations() { // Allocate multiple blocks for i in 0..5 { - let size = 64 + i * 32; // Different sizes - let args = [size, 4, 0, 0, 0, 0]; // 4-byte alignment - + let size = 64 + i * 32; + let args = [size, 4, 0, 0, 0, 0]; + let (ptr, _) = syscall_handler.handle_syscall( SYSCALL_ALLOC, args, @@ -78,28 +70,27 @@ fn test_multiple_allocations() { &mut regs, &mut meter, ); - - println!("✅ Allocation {}: size={}, ptr=0x{:08x}", i, size, ptr); + assert_ne!(ptr, 0); pointers.push(ptr); } - // Verify pointers are different and properly aligned - for (i, &ptr) in pointers.iter().enumerate() { - assert!(ptr % 4 == 0, "Allocation {} not aligned: 0x{:08x}", i, ptr); + // Verify pointers are aligned + for &ptr in &pointers { + assert_eq!(ptr % 4, 0); } // Verify no overlapping pointers (simple check) for i in 0..pointers.len() { - for j in i+1..pointers.len() { - assert_ne!(pointers[i], pointers[j], "Duplicate pointers: 0x{:08x}", pointers[i]); + for j in i + 1..pointers.len() { + assert_ne!(pointers[i], pointers[j]); } } } #[test] fn test_alignment_requirements() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); + let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); @@ -108,7 +99,7 @@ fn test_alignment_requirements() { // Test various alignments let alignments = [1, 2, 4, 8, 16]; - + for &align in &alignments { let args = [256, align as u32, 0, 0, 0, 0]; let (ptr, _) = syscall_handler.handle_syscall( @@ -120,17 +111,15 @@ fn test_alignment_requirements() { &mut regs, &mut meter, ); - - println!("✅ Alignment test: align={}, ptr=0x{:08x}", align, ptr); + assert_ne!(ptr, 0); - assert!(ptr as usize % align == 0, - "Pointer 0x{:08x} not aligned to {} bytes", ptr, align); + assert_eq!(ptr as usize % align, 0); } } #[test] fn test_invalid_alignment() { - let memory = Rc::new(RefCell::new(memory_page::MemoryPage::new(8192))); + let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); @@ -139,7 +128,7 @@ fn test_invalid_alignment() { // Test invalid alignments (not powers of 2) let invalid_alignments = [0, 3, 5, 6, 7, 9]; - + for &align in &invalid_alignments { let args = [100, align as u32, 0, 0, 0, 0]; let (ptr, _) = syscall_handler.handle_syscall( @@ -151,8 +140,7 @@ fn test_invalid_alignment() { &mut regs, &mut meter, ); - - println!("✅ Invalid alignment test: align={}, ptr=0x{:08x}", align, ptr); - assert_eq!(ptr, 0, "Should return null for invalid alignment {}", align); + + assert_eq!(ptr, 0); } } diff --git a/crates/vm/tests/memory_page_offset.rs b/crates/avm/tests/memory_page_offset.rs similarity index 68% rename from crates/vm/tests/memory_page_offset.rs rename to crates/avm/tests/memory_page_offset.rs index 5eb8833..fc6e2c7 100644 --- a/crates/vm/tests/memory_page_offset.rs +++ b/crates/avm/tests/memory_page_offset.rs @@ -1,7 +1,5 @@ -use vm::memory_page::MemoryPage; -use vm::vm::VM; -use vm::sys_call::DefaultSyscallHandler; -use vm::metering::{NoopMeter, MemoryAccessKind}; +use avm::memory::MemoryPage; +use vm::metering::{MemoryAccessKind, NoopMeter}; #[test] fn test_offset_zero_base() { @@ -35,11 +33,20 @@ fn test_store_and_load_zero_base() { let mem = MemoryPage::new_with_base(1024, 0); let mut meter = NoopMeter::default(); assert!(mem.store_u8(10, 0xAB, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_byte(10, &mut meter, MemoryAccessKind::Load), Some(0xAB)); + assert_eq!( + mem.load_byte(10, &mut meter, MemoryAccessKind::Load), + Some(0xAB) + ); assert!(mem.store_u16(20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_halfword(20, &mut meter, MemoryAccessKind::Load), Some(0xCDEF)); + assert_eq!( + mem.load_halfword(20, &mut meter, MemoryAccessKind::Load), + Some(0xCDEF) + ); assert!(mem.store_u32(30, 0x12345678, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_u32(30, &mut meter, MemoryAccessKind::Load), Some(0x12345678)); + assert_eq!( + mem.load_u32(30, &mut meter, MemoryAccessKind::Load), + Some(0x12345678) + ); } #[test] @@ -48,11 +55,20 @@ fn test_store_and_load_high_base() { let mem = MemoryPage::new_with_base(1024, base); let mut meter = NoopMeter::default(); assert!(mem.store_u8(base + 10, 0xAB, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_byte(base + 10, &mut meter, MemoryAccessKind::Load), Some(0xAB)); + assert_eq!( + mem.load_byte(base + 10, &mut meter, MemoryAccessKind::Load), + Some(0xAB) + ); assert!(mem.store_u16(base + 20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_halfword(base + 20, &mut meter, MemoryAccessKind::Load), Some(0xCDEF)); + assert_eq!( + mem.load_halfword(base + 20, &mut meter, MemoryAccessKind::Load), + Some(0xCDEF) + ); assert!(mem.store_u32(base + 30, 0x12345678, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_u32(base + 30, &mut meter, MemoryAccessKind::Load), Some(0x12345678)); + assert_eq!( + mem.load_u32(base + 30, &mut meter, MemoryAccessKind::Load), + Some(0x12345678) + ); } #[test] @@ -61,5 +77,8 @@ fn test_store_and_load_at_offset_zero() { let mem = MemoryPage::new_with_base(1024, base); let mut meter = NoopMeter::default(); assert!(mem.store_u8(base, 0xAA, &mut meter, MemoryAccessKind::Store)); - assert_eq!(mem.load_byte(base, &mut meter, MemoryAccessKind::Load), Some(0xAA)); + assert_eq!( + mem.load_byte(base, &mut meter, MemoryAccessKind::Load), + Some(0xAA) + ); } diff --git a/crates/vm/tests/spec_runner.rs b/crates/avm/tests/spec_runner.rs similarity index 63% rename from crates/vm/tests/spec_runner.rs rename to crates/avm/tests/spec_runner.rs index 5ab937d..e9a212d 100644 --- a/crates/vm/tests/spec_runner.rs +++ b/crates/avm/tests/spec_runner.rs @@ -3,15 +3,26 @@ use std::io::Read; use std::path::Path; +use avm::memory::MemoryPage; use vm::vm::VM; mod test_syscall_handler; use test_syscall_handler::TestSyscallHandler; +use vm::memory::SharedMemory; /// Tests that are skipped and the reasons why const SKIPPED_TESTS: &[(&str, &str)] = &[ - ("fence_i", "Requires self-modifying code support (writes instructions to memory and executes them)"), - ("ld_st", "Contains 64-bit load/store instructions (ld/sd) that the 32-bit VM doesn't support"), - ("st_ld", "Contains 64-bit store/load instructions (sd/ld) that the 32-bit VM doesn't support"), + ( + "fence_i", + "Requires self-modifying code support (writes instructions to memory and executes them)", + ), + ( + "ld_st", + "Contains 64-bit load/store instructions (ld/sd) that the 32-bit VM doesn't support", + ), + ( + "st_ld", + "Contains 64-bit store/load instructions (sd/ld) that the 32-bit VM doesn't support", + ), ("lrsc", "LR/SC implementation needs improvement - causes infinite loops"), ]; @@ -42,9 +53,13 @@ fn run_single_test(elf_path: &str) -> Result<(), Box> { // Parse ELF let elf = compiler::elf::parse_elf_from_bytes(&elf_bytes)?; - let (code, code_start) = elf.get_flat_code().ok_or("No code section in ELF")?; - let (rodata, rodata_start) = elf.get_flat_rodata().unwrap_or((vec![], usize::MAX as u64)); - + let (code, code_start) = elf + .get_flat_code() + .ok_or("No code section in ELF")?; + let (rodata, rodata_start) = elf + .get_flat_rodata() + .unwrap_or((vec![], usize::MAX as u64)); + // Get .data section if it exists let (data, data_start) = if let Some(data_section) = elf.get_section_by_name(".data") { (data_section.data.to_vec(), data_section.addr as usize) @@ -54,53 +69,65 @@ fn run_single_test(elf_path: &str) -> Result<(), Box> { // Find .tohost section if let Some(tohost_section) = elf.get_section_by_name(".tohost") { - println!(".tohost section found at addr=0x{:x}, size=0x{:x}", tohost_section.addr, tohost_section.size); + println!( + ".tohost section found at addr=0x{:x}, size=0x{:x}", + tohost_section.addr, tohost_section.size + ); } else { println!(".tohost section not found, skipping..."); return Ok(()); } // Set up VM memory (allocate enough to cover 0x80000000+) - let memory = std::rc::Rc::new(std::cell::RefCell::new(vm::memory_page::MemoryPage::new_with_base(0x20000, 0x80000000))); // 128KB at 0x80000000 - println!("Loading code into VM: addr=0x{:x}, size=0x{:x}", code_start, code.len()); + let memory: SharedMemory = std::rc::Rc::new(MemoryPage::new_with_base( + 0x20000, + 0x80000000, + )); // 128KB at 0x80000000 + println!( + "Loading code into VM: addr=0x{:x}, size=0x{:x}", + code_start, + code.len() + ); // Set up VM let storage = std::rc::Rc::new(std::cell::RefCell::new(storage::Storage::default())); - let host: Box = Box::new(vm::host_interface::NoopHost {}); + let host: Box = + Box::new(vm::host_interface::NoopHost {}); // When constructing the VM, use the test syscall handler: let mut syscall_handler = Box::new(TestSyscallHandler::new()); - + // Set .tohost address if found if let Some(tohost_section) = elf.get_section_by_name(".tohost") { syscall_handler.set_tohost_addr(tohost_section.addr); syscall_handler.set_memory(memory.clone()); } - + // Move the handler into the VM, then extract it after run - let mut vm = VM::new_with_syscall_handler( - memory.clone(), - storage, - host, - syscall_handler, - ); + let mut vm = VM::new_with_syscall_handler(memory.clone(), storage, host, syscall_handler); vm.cpu.verbose = false; // Set to false to reduce output for multiple tests vm.set_code(code_start as u32, code_start as u32, &code); if !rodata.is_empty() { - println!("Writing rodata to memory: addr=0x{:x}, size=0x{:x}", rodata_start, rodata.len()); - memory.borrow_mut().write_code(rodata_start as usize, &rodata); + println!( + "Writing rodata to memory: addr=0x{:x}, size=0x{:x}", + rodata_start, rodata.len() + ); + memory.write_code(rodata_start as usize, &rodata); } if !data.is_empty() { - println!("Writing data to memory: addr=0x{:x}, size=0x{:x}", data_start, data.len()); - memory.borrow_mut().write_code(data_start as usize, &data); + println!( + "Writing data to memory: addr=0x{:x}, size=0x{:x}", + data_start, data.len() + ); + memory.write_code(data_start as usize, &data); } // Run the VM println!("Running test..."); vm.raw_run(); println!("Test completed."); - + Ok(()) } @@ -108,10 +135,10 @@ fn run_single_test(elf_path: &str) -> Result<(), Box> { fn collect_test_files(test_dir: &str, category: &str) -> (Vec, usize) { let mut test_files = Vec::new(); let mut skipped_count = 0; - + println!("Looking for files in: {}", test_dir); println!("Category prefix: rv32{}p-", category); - + if let Ok(entries) = std::fs::read_dir(test_dir) { for entry in entries { if let Ok(entry) = entry { @@ -120,17 +147,17 @@ fn collect_test_files(test_dir: &str, category: &str) -> (Vec, usize) { if let Some(name_str) = file_name.to_str() { // Include files that start with the category prefix and are not .dump files let category_prefix = format!("rv32{}-p-", category); - if name_str.starts_with(&category_prefix) && - !path.is_dir() && - !name_str.ends_with(".dump") { - + if name_str.starts_with(&category_prefix) + && !path.is_dir() + && !name_str.ends_with(".dump") + { // Check if this test should be skipped if let Some(reason) = should_skip_test(name_str) { println!("Skipping {}: {}", name_str, reason); skipped_count += 1; continue; } - + test_files.push(path.to_string_lossy().to_string()); } } @@ -140,17 +167,25 @@ fn collect_test_files(test_dir: &str, category: &str) -> (Vec, usize) { } else { println!("Failed to read directory: {}", test_dir); } - + test_files.sort(); // Sort for consistent ordering (test_files, skipped_count) } /// Run all tests for a specific category -fn run_category_tests(test_dir: &str, category: &str) -> Result<(usize, usize, usize), Box> { +fn run_category_tests( + test_dir: &str, + category: &str, +) -> Result<(usize, usize, usize), Box> { println!("\n=== Running {} category tests ===", category.to_uppercase()); - + let (test_files, skipped_count) = collect_test_files(test_dir, category); - println!("Found {} {} test files to run ({} skipped)", test_files.len(), category, skipped_count); + println!( + "Found {} {} test files to run ({} skipped)", + test_files.len(), + category, + skipped_count + ); let mut passed_count = 0; let mut failed_count = 0; @@ -161,9 +196,9 @@ fn run_category_tests(test_dir: &str, category: &str) -> Result<(usize, usize, u .unwrap() .to_str() .unwrap(); - + print!("[{:2}/{:2}] {}: ", i + 1, test_files.len(), test_name); - + if let Err(e) = run_single_test(elf_path) { println!("❌ FAILED - {}", e); failed_count += 1; @@ -173,7 +208,7 @@ fn run_category_tests(test_dir: &str, category: &str) -> Result<(usize, usize, u passed_count += 1; } } - + println!("=== {} category tests completed ===", category.to_uppercase()); Ok((passed_count, failed_count, skipped_count)) } @@ -182,7 +217,7 @@ fn run_category_tests(test_dir: &str, category: &str) -> Result<(usize, usize, u fn test_riscv_spec() { // Discover all test files in the riscv-tests directory let test_dir = "tests/riscv-tests-install/share/riscv-tests/isa"; - + // Print current working directory for debugging println!("Current dir: {:?}", std::env::current_dir().unwrap()); println!("Looking for tests in: {}", test_dir); @@ -216,52 +251,33 @@ fn test_riscv_spec() { } } } - + // Print comprehensive summary - println!("\n{}", "=".repeat(60)); - println!("📊 RISC-V SPECIFICATION TEST SUITE SUMMARY"); + println!("\n📊 Test Suite Summary"); println!("{}", "=".repeat(60)); - - // Category breakdown - println!("\n📋 Category Breakdown:"); - for (category, passed, failed, skipped) in &category_results { - let total = passed + failed + skipped; - let success_rate = if total > 0 { (*passed as f64 / total as f64) * 100.0 } else { 0.0 }; - println!(" {}: {}/{} passed ({:.1}%) {} skipped", - category.to_uppercase(), passed, total, success_rate, skipped); - } - - // Overall statistics - let total_tests = total_passed + total_failed + total_skipped; - let overall_success_rate = if total_tests > 0 { (total_passed as f64 / total_tests as f64) * 100.0 } else { 0.0 }; - - println!("\n📈 Overall Statistics:"); - println!(" Total Tests: {}", total_tests); - println!(" Passed: {} ✅", total_passed); - println!(" Failed: {} ❌", total_failed); - println!(" Skipped: {} ⏭️", total_skipped); - println!(" Success Rate: {:.1}%", overall_success_rate); - - // Test coverage information - println!("\n🎯 Test Coverage:"); - println!(" UI Tests: Base integer instructions (RV32I)"); - println!(" UM Tests: Integer multiplication and division (RV32M)"); - println!(" UA Tests: Atomic memory operations (RV32A)"); - println!(" UC Tests: Compressed instructions (RV32C)"); - - // Skipped tests explanation - if total_skipped > 0 { - println!("\n⏭️ Skipped Tests:"); - for (test_name, reason) in SKIPPED_TESTS { - println!(" - {}: {}", test_name, reason); - } + println!( + "{:<10} | {:<8} | {:<8} | {:<8}", + "Category", "Passed", "Failed", "Skipped" + ); + println!("{}", "-".repeat(60)); + for (category, passed, failed, skipped) in category_results { + println!( + "{:<10} | {:<8} | {:<8} | {:<8}", + category.to_uppercase(), + passed, + failed, + skipped + ); } - - println!("\n{}", "=".repeat(60)); - - if total_failed > 0 { - panic!("Test suite completed with {} failures", total_failed); + println!("{}", "-".repeat(60)); + println!( + "TOTAL | {:<8} | {:<8} | {:<8}", + total_passed, total_failed, total_skipped + ); + + if total_failed == 0 { + println!("🎉 All tests passed! Great job!"); } else { - println!("🎉 All tests passed successfully!"); + panic!("❌ Some tests failed. Please review the results above."); } -} \ No newline at end of file +} diff --git a/crates/vm/tests/test_syscall_handler.rs b/crates/avm/tests/test_syscall_handler.rs similarity index 78% rename from crates/vm/tests/test_syscall_handler.rs rename to crates/avm/tests/test_syscall_handler.rs index c9d56a6..7248ddd 100644 --- a/crates/vm/tests/test_syscall_handler.rs +++ b/crates/avm/tests/test_syscall_handler.rs @@ -1,12 +1,12 @@ +use std::any::Any; +use std::cell::RefCell; use std::rc::Rc; -use core::cell::RefCell; -use vm::memory_page::MemoryPage; use storage::Storage; use vm::host_interface::HostInterface; -use vm::sys_call::SyscallHandler; -use vm::registers::Register; -use std::any::Any; use vm::metering::Metering; +use vm::memory::SharedMemory; +use vm::registers::Register; +use vm::sys_call::SyscallHandler; /// Map RISC-V test exit codes to test case numbers /// Formula: exit_code = (TESTNUM << 1) | 1 @@ -24,12 +24,15 @@ fn exit_code_to_test_num(exit_code: u32) -> Option { #[derive(Debug)] pub struct TestSyscallHandler { tohost_addr: u64, - memory: Option>>, + memory: Option, } impl TestSyscallHandler { pub fn new() -> Self { - Self { tohost_addr: 0, memory: None } + Self { + tohost_addr: 0, + memory: None, + } } /// Set the address of the .tohost section @@ -38,7 +41,7 @@ impl TestSyscallHandler { } /// Set the memory reference (needed to read .tohost) - pub fn set_memory(&mut self, memory: Rc>) { + pub fn set_memory(&mut self, memory: SharedMemory) { self.memory = Some(memory); } } @@ -51,7 +54,7 @@ impl SyscallHandler for TestSyscallHandler { &mut self, call_id: u32, _args: [u32; 6], - memory: Rc>, + memory: SharedMemory, _storage: Rc>, _host: &mut Box, regs: &mut [u32; 32], @@ -62,11 +65,11 @@ impl SyscallHandler for TestSyscallHandler { SYSCALL_TEST_DONE => { // Read .tohost value let mem_ref = self.memory.as_ref().unwrap_or(&memory); - let offset = mem_ref.borrow().offset(self.tohost_addr as usize); - let mem_guard = mem_ref.borrow(); - let mem = mem_guard.mem(); + let offset = mem_ref.offset(self.tohost_addr as usize); + let mem = mem_ref.mem(); if offset + 8 <= mem.len() { - let tohost_val = u64::from_le_bytes(mem[offset..offset+8].try_into().unwrap()); + let tohost_val = + u64::from_le_bytes(mem[offset..offset + 8].try_into().unwrap()); // Use .tohost value as the test result result = tohost_val as u32; } else { @@ -74,21 +77,24 @@ impl SyscallHandler for TestSyscallHandler { } if result == 0 { return (result, true); - } + } panic!("[spec-test] FAIL: .tohost value = 0x{:x}", result); - }, + } SYSCALL_TERMINATE => { let exit_code = regs[Register::A0 as usize]; if exit_code != 0 { // Try to map exit code to test case number if let Some(test_num) = exit_code_to_test_num(exit_code) { - panic!("[spec-test] FAIL: Test case {} failed (exit code {})", test_num, exit_code); + panic!( + "[spec-test] FAIL: Test case {} failed (exit code {})", + test_num, exit_code + ); } else { panic!("[spec-test] FAIL: Test failed with exit code {}", exit_code); } } return (exit_code, false); // halt VM - }, + } _ => { panic!("Unknown syscall ID: {}", call_id); } @@ -97,4 +103,4 @@ impl SyscallHandler for TestSyscallHandler { fn as_any(&self) -> &dyn Any { self } -} +} diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index bf3c1cf..3593161 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -1,14 +1,14 @@ -use crate::decoder::{decode_full, decode_compressed}; -use crate::instruction::Instruction; -use crate::memory_page::MemoryPage; -use storage::Storage; -use std::rc::Rc; -use core::cell::RefCell; +use crate::decoder::{decode_compressed, decode_full}; use crate::host_interface::HostInterface; +use crate::instruction::Instruction; +use crate::memory::SharedMemory; +use crate::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; use crate::sys_call::SyscallHandler; +use core::cell::RefCell; use core::fmt::Write; use std::collections::HashMap; -use crate::metering::{Metering, MeterResult, NoopMeter, MemoryAccessKind}; +use std::rc::Rc; +use storage::Storage; #[path = "exe.rs"] mod exec; @@ -37,11 +37,11 @@ mod exec; /// This allows us to run programs written for one architecture (RISC-V) on /// different hardware (like x86 or ARM). The VM provides an abstraction layer /// that makes the underlying hardware details transparent to the running program. -/// -/// MEMORY MANAGEMENT: We use Rc> for shared mutable access to memory -/// and storage, which allows the CPU to read/write memory while maintaining -/// Rust's safety guarantees. -/// +/// +/// MEMORY MANAGEMENT: We use Rc-backed trait objects for shared memory and +/// Rc> for storage, which allows the CPU to read/write memory +/// while maintaining Rust's safety guarantees. +/// /// PERFORMANCE CONSIDERATIONS: This is an interpretive VM, meaning each /// instruction is decoded and executed one at a time. Real CPUs use techniques /// like pipelining, out-of-order execution, and just-in-time compilation to @@ -88,7 +88,10 @@ impl std::fmt::Debug for CPU { .field("regs", &self.regs) .field("verbose", &self.verbose) .field("reservation_addr", &self.reservation_addr) - .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "Some()")) + .field( + "verbose_writer", + &self.verbose_writer.as_ref().map(|_| "Some()"), + ) .field("metering", &"") .finish() } @@ -210,7 +213,7 @@ impl CPU { /// redirects the flow (like a branch or jump instruction). pub fn step( &mut self, - memory: Rc>, + memory: SharedMemory, storage: Rc>, host: &mut Box, ) -> bool { @@ -245,19 +248,35 @@ impl CPU { /// - memory: Shared reference to memory for load/store operations /// - storage: Shared reference to persistent storage fn run_instruction( - &mut self, - instr: Instruction, - size: u8, - memory: Rc>, + &mut self, + instr: Instruction, + size: u8, + memory: SharedMemory, storage: Rc>, - host: &mut Box) -> bool { + host: &mut Box, + ) -> bool { // EDUCATIONAL: Debug output to help understand what's happening // Get the actual instruction bytes for debugging - if let Some(bytes) = memory.borrow().mem_slice(self.pc as usize, self.pc as usize + size as usize) { - let hex_bytes = bytes.iter().map(|b| format!("{:02x}", b)).collect::>().join(" "); - self.log(&format!("PC = 0x{:08x}, Bytes = [{}], Instr = {}", self.pc, hex_bytes, instr.pretty_print()), true); + if let Some(bytes) = memory.mem_slice(self.pc as usize, self.pc as usize + size as usize) { + let hex_bytes = bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(" "); + self.log( + &format!( + "PC = 0x{:08x}, Bytes = [{}], Instr = {}", + self.pc, + hex_bytes, + instr.pretty_print() + ), + true, + ); } else { - self.log(&format!("PC = 0x{:08x}, Instr = {}", self.pc, instr.pretty_print()), true); + self.log( + &format!("PC = 0x{:08x}, Instr = {}", self.pc, instr.pretty_print()), + true, + ); } if !Self::can_continue(self.metering.on_instruction(self.pc, &instr, size)) { @@ -266,9 +285,9 @@ impl CPU { // EDUCATIONAL: Remember the old PC to detect if the instruction changed it let old_pc = self.pc; - + // EDUCATIONAL: Execute the instruction - let result = self.execute(instr, memory, storage, host); + let result = self.execute(instr, memory, storage, host); // EDUCATIONAL: Only increment PC if the instruction didn't change it // This handles branches, jumps, and calls correctly @@ -281,28 +300,32 @@ impl CPU { } /// Handles unknown or invalid instructions. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates error handling in CPU design. /// When a CPU encounters an invalid instruction, it needs to handle it /// gracefully rather than crashing. - /// + /// /// DEBUGGING: This function provides detailed information about what /// went wrong, including the hex dump of the invalid bytes. - /// + /// /// RETURN VALUE: Returns false to halt execution on invalid instructions - fn unknown_instruction(&mut self, memory: Rc>, _storage: Rc>) -> bool { + fn unknown_instruction( + &mut self, + memory: SharedMemory, + _storage: Rc>, + ) -> bool { // EDUCATIONAL: Try to read the invalid instruction bytes for debugging - if let Some(slice_ref) = memory.borrow().mem_slice(self.pc as usize, self.pc as usize + 4) { + if let Some(slice_ref) = memory.mem_slice(self.pc as usize, self.pc as usize + 4) { // EDUCATIONAL: Convert bytes to hex for human-readable debugging - let hex_dump = slice_ref.iter() + let hex_dump = slice_ref + .iter() .map(|b| format!("{:02x}", b)) // still needs deref .collect::>() .join(" "); panic!( "🚨 Unknown or invalid instruction at PC = 0x{:08x} (bytes: [{}])", - self.pc, - hex_dump + self.pc, hex_dump ); } else { panic!( @@ -314,22 +337,21 @@ impl CPU { } /// Fetches and decodes the next instruction from memory. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates the fetch and decode phases /// of the instruction cycle. It handles both regular (32-bit) and /// compressed (16-bit) RISC-V instructions. - /// + /// /// RISC-V COMPRESSED INSTRUCTIONS: RISC-V supports 16-bit compressed /// instructions to reduce code size. The bottom 2 bits determine if /// an instruction is compressed (not 0b11) or regular (0b11). - /// + /// /// RETURN VALUE: Returns Some((instruction, size)) if successful, None if invalid - pub fn next_instruction(&mut self, memory: Rc>) -> Option<(Instruction, u8)> { + pub fn next_instruction(&mut self, memory: SharedMemory) -> Option<(Instruction, u8)> { let pc = self.pc as usize; - let mem_ref = memory.borrow(); - + // EDUCATIONAL: Read 4 bytes from memory (enough for any instruction) - let bytes = mem_ref.mem_slice(pc, pc + 4)?; + let bytes = memory.mem_slice(pc, pc + 4)?; // EDUCATIONAL: Need at least 2 bytes for any instruction if bytes.len() < 2 { @@ -386,7 +408,10 @@ impl CPU { /// Add to the stack pointer (x2) with metering. fn sp_add(&mut self, delta: u32) -> bool { - let sp = match self.read_reg(2) { Some(v) => v, None => return false }; + let sp = match self.read_reg(2) { + Some(v) => v, + None => return false, + }; self.write_reg(2, sp.wrapping_add(delta)) } diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index 9bbfd73..1adb838 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,7 +1,7 @@ -use std::rc::Rc; use core::cell::RefCell; +use std::rc::Rc; -use super::{CPU, Instruction, MemoryAccessKind, MemoryPage}; +use super::{Instruction, MemoryAccessKind, SharedMemory, CPU}; use crate::host_interface::HostInterface; use crate::instruction::CsrOp; use crate::registers::Register; @@ -30,7 +30,7 @@ impl CPU { pub fn execute( &mut self, instr: Instruction, - memory: Rc>, + memory: SharedMemory, storage: Rc>, host: &mut Box, ) -> bool { @@ -39,196 +39,391 @@ impl CPU { Instruction::Add { rd, rs1, rs2 } => { // EDUCATIONAL: Use wrapping_add to handle overflow correctly // In real CPUs, overflow might set flags or cause exceptions - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs.wrapping_add(rhs)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_add(rhs)) { + return false; + } } Instruction::Sub { rd, rs1, rs2 } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { + return false; + } } Instruction::Addi { rd, rs1, imm } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs.wrapping_add(imm as u32)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_add(imm as u32)) { + return false; + } } // EDUCATIONAL: Logical instructions - perform bitwise operations Instruction::And { rd, rs1, rs2 } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs & rhs) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs & rhs) { + return false; + } } Instruction::Or { rd, rs1, rs2 } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs | rhs) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs | rhs) { + return false; + } } Instruction::Xor { rd, rs1, rs2 } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs ^ rhs) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs ^ rhs) { + return false; + } } Instruction::Andi { rd, rs1, imm } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs & (imm as u32)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs & (imm as u32)) { + return false; + } } Instruction::Ori { rd, rs1, imm } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs | (imm as u32)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs | (imm as u32)) { + return false; + } } Instruction::Xori { rd, rs1, imm } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs ^ (imm as u32)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs ^ (imm as u32)) { + return false; + } } // EDUCATIONAL: Comparison instructions - set result to 0 or 1 Instruction::Slt { rd, rs1, rs2 } => { // EDUCATIONAL: Set if less than (signed comparison) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, (lhs as i32).lt(&(rhs as i32)) as u32) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, (lhs as i32).lt(&(rhs as i32)) as u32) { + return false; + } } Instruction::Sltu { rd, rs1, rs2 } => { // EDUCATIONAL: Set if less than (unsigned comparison) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, (lhs < rhs) as u32) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, (lhs < rhs) as u32) { + return false; + } } Instruction::Slti { rd, rs1, imm } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, (lhs as i32).lt(&imm) as u32) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, (lhs as i32).lt(&imm) as u32) { + return false; + } } Instruction::Sltiu { rd, rs1, imm } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let rhs = imm as u32; - if !self.write_reg(rd, if lhs < rhs { 1 } else { 0 }) { return false; } + if !self.write_reg(rd, if lhs < rhs { 1 } else { 0 }) { + return false; + } } // EDUCATIONAL: Shift instructions - move bits left or right Instruction::Sll { rd, rs1, rs2 } => { // EDUCATIONAL: Logical left shift - multiply by 2^shift_amount // The & 0x1F ensures shift amount is 0-31 (5 bits) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs << (rhs & 0x1F)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs << (rhs & 0x1F)) { + return false; + } } Instruction::Srl { rd, rs1, rs2 } => { // EDUCATIONAL: Logical right shift - divide by 2^shift_amount - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs >> (rhs & 0x1F)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs >> (rhs & 0x1F)) { + return false; + } } Instruction::Sra { rd, rs1, rs2 } => { // EDUCATIONAL: Arithmetic right shift - preserves sign bit - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, ((lhs as i32) >> (rhs & 0x1F)) as u32) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, ((lhs as i32) >> (rhs & 0x1F)) as u32) { + return false; + } } Instruction::Slli { rd, rs1, shamt } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs << shamt) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs << shamt) { + return false; + } } Instruction::Srli { rd, rs1, shamt } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs >> shamt) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs >> shamt) { + return false; + } } Instruction::Srai { rd, rs1, shamt } => { - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.write_reg(rd, ((lhs as i32) >> shamt) as u32) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, ((lhs as i32) >> shamt) as u32) { + return false; + } } // EDUCATIONAL: Load instructions - read data from memory into registers Instruction::Lw { rd, rs1, offset } => { // EDUCATIONAL: Load word (32-bit) from memory // Address = base register + offset - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let addr = base.wrapping_add(offset as u32) as usize; - let val = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; - if !self.write_reg(rd, val) { return false; } + let addr = base.wrapping_add(offset as u32) as usize; + let val = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, val) { + return false; + } } Instruction::Ld { rd, rs1, offset } => { // EDUCATIONAL: Load doubleword (64-bit) from memory, truncated to 32-bit // Since this is a 32-bit VM, we only load the lower 32 bits - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let addr = base.wrapping_add(offset as u32) as usize; - let val = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; - if !self.write_reg(rd, val) { return false; } + let addr = base.wrapping_add(offset as u32) as usize; + let val = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, val) { + return false; + } } Instruction::Lb { rd, rs1, offset } => { // EDUCATIONAL: Load byte (8-bit, sign-extended) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let addr = base.wrapping_add(offset as u32) as usize; - let byte = match memory.borrow().load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; + let addr = base.wrapping_add(offset as u32) as usize; + let byte = + match memory.load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; let value = (byte as i8) as i32 as u32; // sign-extend to 32-bit - if !self.write_reg(rd, value) { return false; } + if !self.write_reg(rd, value) { + return false; + } } Instruction::Lbu { rd, rs1, offset } => { // EDUCATIONAL: Load byte unsigned (8-bit, zero-extended) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let addr = base.wrapping_add(offset as u32) as usize; - let byte = match memory.borrow().load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; - if !self.write_reg(rd, byte as u32) { return false; } + let addr = base.wrapping_add(offset as u32) as usize; + let byte = + match memory.load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, byte as u32) { + return false; + } } Instruction::Lh { rd, rs1, offset } => { // EDUCATIONAL: Load halfword (16-bit, sign-extended) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base.wrapping_add(offset as u32) as usize; - let halfword = match memory.borrow().load_halfword(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + let halfword = match memory.load_halfword( + addr, + self.metering.as_mut(), + MemoryAccessKind::Load, + ) { Some(v) => v, None => return false, }; let value = (halfword as i16) as i32 as u32; // sign-extend to 32-bit - if !self.write_reg(rd, value) { return false; } + if !self.write_reg(rd, value) { + return false; + } } Instruction::Lhu { rd, rs1, offset } => { // EDUCATIONAL: Load halfword unsigned (16-bit, zero-extended) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base.wrapping_add(offset as u32) as usize; - let halfword = match memory.borrow().load_halfword(addr, self.metering.as_mut(), MemoryAccessKind::Load) { + let halfword = match memory.load_halfword( + addr, + self.metering.as_mut(), + MemoryAccessKind::Load, + ) { Some(v) => v, None => return false, }; - if !self.write_reg(rd, halfword as u32) { return false; } // zero-extend to 32-bit + if !self.write_reg(rd, halfword as u32) { + return false; + } // zero-extend to 32-bit } // EDUCATIONAL: Store instructions - write data from registers to memory Instruction::Sh { rs1, rs2, offset } => { // EDUCATIONAL: Store halfword (16-bit) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base.wrapping_add(offset as u32) as usize; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !memory.borrow().store_u16(addr, (src & 0xFFFF) as u16, self.metering.as_mut(), MemoryAccessKind::Store) { + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !memory.store_u16( + addr, + (src & 0xFFFF) as u16, + self.metering.as_mut(), + MemoryAccessKind::Store, + ) { return false; } } Instruction::Sw { rs1, rs2, offset } => { // EDUCATIONAL: Store word (32-bit) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base.wrapping_add(offset as u32) as usize; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !memory.borrow().store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Store) { + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !memory.store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Store) { return false; } } Instruction::Sb { rs1, rs2, offset } => { // EDUCATIONAL: Store byte (8-bit) - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base.wrapping_add(offset as u32) as usize; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !memory.borrow().store_u8(addr, (src & 0xFF) as u8, self.metering.as_mut(), MemoryAccessKind::Store) { + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !memory.store_u8( + addr, + (src & 0xFF) as u8, + self.metering.as_mut(), + MemoryAccessKind::Store, + ) { return false; } } @@ -237,83 +432,151 @@ impl CPU { // These implement if/else and loop constructs Instruction::Beq { rs1, rs2, offset } => { // EDUCATIONAL: Branch if equal - jump if two registers are equal - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if lhs == rhs { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } Instruction::Bne { rs1, rs2, offset } => { // EDUCATIONAL: Branch if not equal - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if lhs != rhs { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } Instruction::Blt { rs1, rs2, offset } => { // EDUCATIONAL: Branch if less than (signed comparison) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if (lhs as i32) < (rhs as i32) { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } Instruction::Bge { rs1, rs2, offset } => { // EDUCATIONAL: Branch if greater than or equal (signed) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if (lhs as i32) >= (rhs as i32) { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } Instruction::Bltu { rs1, rs2, offset } => { // EDUCATIONAL: Branch if less than (unsigned comparison) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if lhs < rhs { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } Instruction::Bgeu { rs1, rs2, offset } => { // EDUCATIONAL: Branch if greater than or equal (unsigned) - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if lhs >= rhs { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } // EDUCATIONAL: Jump and Link instructions - for function calls - Instruction::Jal { rd, offset, compressed } => { + Instruction::Jal { + rd, + offset, + compressed, + } => { // EDUCATIONAL: JAL (Jump and Link) - unconditional jump with return address // Used for function calls and long-distance jumps // The return address is stored in rd (usually x1/ra) let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; - if !self.write_reg(rd, return_address) { return false; } - if !self.pc_add(offset as u32) { return false; } + if !self.write_reg(rd, return_address) { + return false; + } + if !self.pc_add(offset as u32) { + return false; + } return true; } - Instruction::Jalr { rd, rs1, offset , compressed} => { + Instruction::Jalr { + rd, + rs1, + offset, + compressed, + } => { // EDUCATIONAL: JALR (Jump and Link Register) - indirect function calls // Target address = base register + offset, with bottom bit cleared // This ensures proper alignment and is required by RISC-V spec - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let target = base.wrapping_add(offset as u32) & !1; // For compressed instructions (c.jalr), return address should be pc + 2 // For regular instructions (jalr), return address should be pc + 4 let return_address = if compressed { self.pc + 2 } else { self.pc + 4 }; - if !self.write_reg(rd, return_address) { return false; } + if !self.write_reg(rd, return_address) { + return false; + } - if !self.set_pc(target) { return false; } + if !self.set_pc(target) { + return false; + } return true; } @@ -321,102 +584,182 @@ impl CPU { Instruction::Lui { rd, imm } => { // EDUCATIONAL: LUI loads a 20-bit immediate into bits 31-12 of rd // This is used to load large constants (like addresses) into registers - if !self.write_reg(rd, (imm << 12) as u32) { return false; } + if !self.write_reg(rd, (imm << 12) as u32) { + return false; + } } Instruction::Auipc { rd, imm } => { // EDUCATIONAL: AUIPC (Add Upper Immediate to PC) - PC-relative addressing // Used for position-independent code and loading addresses relative to PC - if !self.write_reg(rd, self.pc.wrapping_add((imm << 12) as u32)) { return false; } + if !self.write_reg(rd, self.pc.wrapping_add((imm << 12) as u32)) { + return false; + } } // EDUCATIONAL: Multiplication instructions - extended arithmetic Instruction::Mul { rd, rs1, rs2 } => { // EDUCATIONAL: MUL - multiply two registers, store lower 32 bits - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs.wrapping_mul(rhs)) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_mul(rhs)) { + return false; + } } Instruction::Mulh { rd, rs1, rs2 } => { // EDUCATIONAL: MULH - multiply signed, store upper 32 bits // Properly sign-extend 32-bit values to 64-bit for signed multiplication - let val1 = (match self.read_reg(rs1) { Some(v) => v, None => return false } as i32) as i64; - let val2 = (match self.read_reg(rs2) { Some(v) => v, None => return false } as i32) as i64; + let val1 = (match self.read_reg(rs1) { + Some(v) => v, + None => return false, + } as i32) as i64; + let val2 = (match self.read_reg(rs2) { + Some(v) => v, + None => return false, + } as i32) as i64; let result = val1 * val2; - if !self.write_reg(rd, (result >> 32) as u32) { return false; } + if !self.write_reg(rd, (result >> 32) as u32) { + return false; + } } Instruction::Mulhu { rd, rs1, rs2 } => { // EDUCATIONAL: MULHU - multiply unsigned, store upper 32 bits - let lhs = match self.read_reg(rs1) { Some(v) => v as u64, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v as u64, None => return false }; - if !self.write_reg(rd, ((lhs * rhs) >> 32) as u32) { return false; } + let lhs = match self.read_reg(rs1) { + Some(v) => v as u64, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v as u64, + None => return false, + }; + if !self.write_reg(rd, ((lhs * rhs) >> 32) as u32) { + return false; + } } Instruction::Mulhsu { rd, rs1, rs2 } => { // EDUCATIONAL: MULHSU - multiply signed by unsigned, store upper 32 bits // Properly sign-extend first operand to signed 64-bit, keep second as unsigned 64-bit - let val1 = (match self.read_reg(rs1) { Some(v) => v, None => return false } as i32) as i64; - let val2 = match self.read_reg(rs2) { Some(v) => v as u64, None => return false }; + let val1 = (match self.read_reg(rs1) { + Some(v) => v, + None => return false, + } as i32) as i64; + let val2 = match self.read_reg(rs2) { + Some(v) => v as u64, + None => return false, + }; let result = val1 * (val2 as i64); - if !self.write_reg(rd, (result >> 32) as u32) { return false; } + if !self.write_reg(rd, (result >> 32) as u32) { + return false; + } } // EDUCATIONAL: Division and remainder instructions Instruction::Div { rd, rs1, rs2 } => { // EDUCATIONAL: DIV - signed division // RISC-V spec: division by zero returns -1, overflow returns dividend - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if rhs == 0 { - if !self.write_reg(rd, 0xFFFFFFFF) { return false; } // -1 in two's complement + if !self.write_reg(rd, 0xFFFFFFFF) { + return false; + } // -1 in two's complement } else { let dividend = lhs as i32; let divisor = rhs as i32; // Check for overflow: -2^31 / -1 = 2^31 (overflow) if dividend == i32::MIN && divisor == -1 { - if !self.write_reg(rd, lhs) { return false; } // Return dividend on overflow + if !self.write_reg(rd, lhs) { + return false; + } // Return dividend on overflow } else { - if !self.write_reg(rd, (dividend / divisor) as u32) { return false; } + if !self.write_reg(rd, (dividend / divisor) as u32) { + return false; + } } } } Instruction::Divu { rd, rs1, rs2 } => { // EDUCATIONAL: DIVU - unsigned division // RISC-V spec: division by zero returns 2^XLEN - 1 - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if rhs == 0 { - if !self.write_reg(rd, 0xFFFFFFFF) { return false; } // 2^32 - 1 + if !self.write_reg(rd, 0xFFFFFFFF) { + return false; + } // 2^32 - 1 } else { - if !self.write_reg(rd, lhs / rhs) { return false; } + if !self.write_reg(rd, lhs / rhs) { + return false; + } } } Instruction::Rem { rd, rs1, rs2 } => { // EDUCATIONAL: REM - signed remainder // RISC-V spec: remainder by zero returns dividend, overflow returns dividend - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if rhs == 0 { - if !self.write_reg(rd, lhs) { return false; } + if !self.write_reg(rd, lhs) { + return false; + } } else { let dividend = lhs as i32; let divisor = rhs as i32; // Check for overflow: -2^31 % -1 = 0 (no overflow, but -2^31 % -1 = 0) if dividend == i32::MIN && divisor == -1 { - if !self.write_reg(rd, 0) { return false; } // Remainder of -2^31 % -1 is 0 + if !self.write_reg(rd, 0) { + return false; + } // Remainder of -2^31 % -1 is 0 } else { - if !self.write_reg(rd, (dividend % divisor) as u32) { return false; } + if !self.write_reg(rd, (dividend % divisor) as u32) { + return false; + } } } } Instruction::Remu { rd, rs1, rs2 } => { // EDUCATIONAL: REMU - unsigned remainder // RISC-V spec: remainder by zero returns dividend - let lhs = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let lhs = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; if rhs == 0 { - if !self.write_reg(rd, lhs) { return false; } + if !self.write_reg(rd, lhs) { + return false; + } } else { - if !self.write_reg(rd, lhs % rhs) { return false; } + if !self.write_reg(rd, lhs % rhs) { + return false; + } } } @@ -424,14 +767,35 @@ impl CPU { Instruction::Ecall => { // Prepare syscall args from registers let args = [ - match self.read_reg(Register::A1 as usize) { Some(v) => v, None => return false }, - match self.read_reg(Register::A2 as usize) { Some(v) => v, None => return false }, - match self.read_reg(Register::A3 as usize) { Some(v) => v, None => return false }, - match self.read_reg(Register::A4 as usize) { Some(v) => v, None => return false }, - match self.read_reg(Register::A5 as usize) { Some(v) => v, None => return false }, - match self.read_reg(Register::A6 as usize) { Some(v) => v, None => return false }, + match self.read_reg(Register::A1 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A2 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A3 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A4 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A5 as usize) { + Some(v) => v, + None => return false, + }, + match self.read_reg(Register::A6 as usize) { + Some(v) => v, + None => return false, + }, ]; - let call_id = match self.read_reg(Register::A7 as usize) { Some(v) => v, None => return false }; + let call_id = match self.read_reg(Register::A7 as usize) { + Some(v) => v, + None => return false, + }; let (result, cont) = self.syscall_handler.handle_syscall( call_id, args, @@ -441,12 +805,30 @@ impl CPU { &mut self.regs, self.metering.as_mut(), ); - if !self.write_reg(Register::A0 as usize, result) { return false; } + if !self.write_reg(Register::A0 as usize, result) { + return false; + } return cont; } - Instruction::Csr { rd, rs1, csr, op, imm } => { - let src = if imm { rs1 as u32 } else { match self.read_reg(rs1) { Some(v) => v, None => return false } }; - let old = match self.read_csr(csr) { Some(v) => v, None => return false }; + Instruction::Csr { + rd, + rs1, + csr, + op, + imm, + } => { + let src = if imm { + rs1 as u32 + } else { + match self.read_reg(rs1) { + Some(v) => v, + None => return false, + } + }; + let old = match self.read_csr(csr) { + Some(v) => v, + None => return false, + }; // Apply CSR op semantics let mut new_val = old; @@ -469,15 +851,19 @@ impl CPU { } if src != 0 || matches!(op, CsrOp::Csrrw) { - if !self.write_csr(csr, new_val) { return false; } + if !self.write_csr(csr, new_val) { + return false; + } } - if rd != 0 && !self.write_reg(rd, old) { return false; } + if rd != 0 && !self.write_reg(rd, old) { + return false; + } } Instruction::Ebreak => { // EDUCATIONAL: EBREAK - Environment Break - for debugging // In real systems, this would trigger a debugger breakpoint - return false + return false; } Instruction::Mret => { // Treat MRET as a simple return/halt in this VM @@ -487,36 +873,58 @@ impl CPU { // EDUCATIONAL: Compressed instruction set (RV32C) - space-saving instructions Instruction::Jr { rs1 } => { // EDUCATIONAL: JR (Jump Register) - compressed jump to register - let target = match self.read_reg(rs1) { Some(v) => v, None => return false }; - if !self.set_pc(target) { return false; } + let target = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + if !self.set_pc(target) { + return false; + } return true; } Instruction::Ret => { // EDUCATIONAL: RET - compressed return instruction // Equivalent to JR x1 (jump to return address register) - let target = match self.read_reg(1) { Some(v) => v, None => return false }; // x1 = ra (return address) + let target = match self.read_reg(1) { + Some(v) => v, + None => return false, + }; // x1 = ra (return address) if target == 0 || target == 0xFFFF_FFFF { return false; // halt if ret target is 0 or invalid } - if !self.set_pc(target) { return false; } + if !self.set_pc(target) { + return false; + } return true; } Instruction::Mv { rd, rs2 } => { // EDUCATIONAL: MV (Move) - compressed register copy - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, src) { return false; } + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, src) { + return false; + } } Instruction::Addi16sp { imm } => { // EDUCATIONAL: ADDI16SP - add immediate to stack pointer // x2 is the stack pointer (SP) - if !self.sp_add(imm as u32) { return false; } + if !self.sp_add(imm as u32) { + return false; + } } Instruction::Addi4spn { rd, imm } => { // EDUCATIONAL: ADDI4SPN - add immediate to SP, store in rd // Used for stack frame setup in function prologues - let sp = match self.read_reg(2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, sp.wrapping_add(imm)) { return false; } + let sp = match self.read_reg(2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, sp.wrapping_add(imm)) { + return false; + } } Instruction::Nop => { // EDUCATIONAL: NOP - No Operation - does nothing @@ -524,17 +932,27 @@ impl CPU { } Instruction::Beqz { rs1, offset } => { // EDUCATIONAL: BEQZ - Branch if Equal to Zero (compressed) - let val = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let val = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; if val == 0 { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } Instruction::Bnez { rs1, offset } => { // EDUCATIONAL: BNEZ - Branch if Not Equal to Zero (compressed) - let val = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let val = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; if val != 0 { - if !self.pc_add(offset as u32) { return false; } + if !self.pc_add(offset as u32) { + return false; + } return true; } } @@ -544,27 +962,59 @@ impl CPU { match op { crate::instruction::MiscAluOp::Sub => { // EDUCATIONAL: C.SUB - compressed subtract - let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { return false; } + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs.wrapping_sub(rhs)) { + return false; + } } crate::instruction::MiscAluOp::Xor => { // EDUCATIONAL: C.XOR - compressed XOR - let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs ^ rhs) { return false; } + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs ^ rhs) { + return false; + } } crate::instruction::MiscAluOp::Or => { // EDUCATIONAL: C.OR - compressed OR - let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs | rhs) { return false; } + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs | rhs) { + return false; + } } crate::instruction::MiscAluOp::And => { // EDUCATIONAL: C.AND - compressed AND - let lhs = match self.read_reg(rd) { Some(v) => v, None => return false }; - let rhs = match self.read_reg(rs2) { Some(v) => v, None => return false }; - if !self.write_reg(rd, lhs & rhs) { return false; } + let lhs = match self.read_reg(rd) { + Some(v) => v, + None => return false, + }; + let rhs = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + if !self.write_reg(rd, lhs & rhs) { + return false; + } } } } @@ -576,159 +1026,312 @@ impl CPU { } // ===== RV32A (Atomics) ===== Instruction::AmoswapW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; - if !memory.borrow().store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + if !memory.store_u32(addr, src, self.metering.as_mut(), MemoryAccessKind::Atomic) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmoaddW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; let new_val = orig.wrapping_add(src); - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmoandW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false, }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; let new_val = orig & src; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmoorW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { Some(v) => v, None => return false, }; + let src = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; let new_val = orig | src; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmoxorW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false, }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; let new_val = orig ^ src; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmomaxW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false, }; - let new_val = if (orig as i32) > (src as i32) { orig } else { src }; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if (orig as i32) > (src as i32) { + orig + } else { + src + }; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmominW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false, }; - let new_val = if (orig as i32) < (src as i32) { orig } else { src }; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; + let new_val = if (orig as i32) < (src as i32) { + orig + } else { + src + }; + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmomaxuW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false, }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; let new_val = if orig > src { orig } else { src }; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } Instruction::AmominuW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; - let src = match self.read_reg(rs2) { Some(v) => v, None => return false }; - let addr = base as usize; - let orig = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; + let src = match self.read_reg(rs2) { Some(v) => v, None => return false, }; + let addr = base as usize; + let orig = + match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { + Some(v) => v, + None => return false, + }; let new_val = if orig < src { orig } else { src }; - if !memory.borrow().store_u32(addr, new_val, self.metering.as_mut(), MemoryAccessKind::Atomic) { + if !memory.store_u32( + addr, + new_val, + self.metering.as_mut(), + MemoryAccessKind::Atomic, + ) { + return false; + } + if !self.write_reg(rd, orig) { return false; } - if !self.write_reg(rd, orig) { return false; } } // ===== RV32A (LR/SC) ===== Instruction::LrW { rd, rs1 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base as usize; - let value = match memory.borrow().load_u32(addr, self.metering.as_mut(), MemoryAccessKind::ReservationLoad) { + let value = match memory.load_u32( + addr, + self.metering.as_mut(), + MemoryAccessKind::ReservationLoad, + ) { Some(v) => v, None => return false, }; - if !self.write_reg(rd, value) { return false; } + if !self.write_reg(rd, value) { + return false; + } // Set reservation for this address self.reservation_addr = Some(addr); } Instruction::ScW { rd, rs1, rs2 } => { - let base = match self.read_reg(rs1) { Some(v) => v, None => return false }; + let base = match self.read_reg(rs1) { + Some(v) => v, + None => return false, + }; let addr = base as usize; - let value_to_store = match self.read_reg(rs2) { Some(v) => v, None => return false }; + let value_to_store = match self.read_reg(rs2) { + Some(v) => v, + None => return false, + }; // Check if we have a valid reservation for this address if self.reservation_addr == Some(addr) { // Reservation is valid, perform the store - if !memory.borrow().store_u32(addr, value_to_store, self.metering.as_mut(), MemoryAccessKind::ReservationStore) { + if !memory.store_u32( + addr, + value_to_store, + self.metering.as_mut(), + MemoryAccessKind::ReservationStore, + ) { return false; } - if !self.write_reg(rd, 0) { return false; } // 0 = success - // Clear the reservation (it's consumed) + if !self.write_reg(rd, 0) { + return false; + } // 0 = success + // Clear the reservation (it's consumed) self.reservation_addr = None; } else { // No valid reservation, fail - if !self.write_reg(rd, 1) { return false; } // 1 = failure + if !self.write_reg(rd, 1) { + return false; + } // 1 = failure } } _ => todo!("unhandled instruction"), diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index ff0238b..adf4b84 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -1,11 +1,11 @@ +pub mod cpu; +pub mod decoder; +pub mod host_interface; +pub mod instruction; pub mod isa; pub mod isa_compressed; -pub mod instruction; -pub mod decoder; -pub mod vm; -pub mod cpu; +pub mod memory; +pub mod metering; pub mod registers; -pub mod memory_page; pub mod sys_call; -pub mod host_interface; -pub mod metering; +pub mod vm; diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory.rs new file mode 100644 index 0000000..424b0f6 --- /dev/null +++ b/crates/vm/src/memory.rs @@ -0,0 +1,26 @@ +use std::cell::Ref; +use std::rc::Rc; +use crate::metering::{Metering, MemoryAccessKind}; + +pub const HEAP_PTR_OFFSET: u32 = 0x100; + +pub trait Memory: std::fmt::Debug { + fn mem(&self) -> Ref>; + fn mem_slice(&self, start: usize, end: usize) -> Option>; + fn store_u16(&self, addr: usize, val: u16, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn store_u32(&self, addr: usize, val: u32, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn store_u8(&self, addr: usize, val: u8, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn load_u32(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_byte(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_halfword(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_word(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn write_code(&self, start_addr: usize, code: &[u8]); + fn alloc_on_heap(&self, data: &[u8]) -> u32; + fn stack_top(&self) -> u32; + fn size(&self) -> usize; + fn offset(&self, addr: usize) -> usize; + fn next_heap(&self) -> u32; + fn set_next_heap(&self, next: u32); +} + +pub type SharedMemory = Rc; diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index b9565bc..9f3488d 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -1,13 +1,13 @@ -use crate::memory_page::{MemoryPage, HEAP_PTR_OFFSET}; -use storage::Storage; +use crate::host_interface::HostInterface; +use crate::memory::{SharedMemory, HEAP_PTR_OFFSET}; +use crate::metering::{MeterResult, Metering}; use crate::registers::Register; -use std::rc::Rc; use core::cell::RefCell; -use crate::host_interface::HostInterface; +use core::fmt::Write; use std::any::Any; +use std::rc::Rc; +use storage::Storage; use types::result::RESULT_SIZE; -use core::fmt::Write; -use crate::metering::{Metering, MeterResult}; /// System call IDs for the VM. pub const SYSCALL_STORAGE_GET: u32 = 1; @@ -21,16 +21,16 @@ pub const SYSCALL_DEALLOC: u32 = 8; pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; /// Represents different types of arguments that can be passed to system calls. -/// +/// /// EDUCATIONAL: This enum demonstrates how to handle different data types /// in system calls. In real operating systems, system calls need to handle /// various data types safely. enum Arg { - U32(u32), // 32-bit unsigned integer - F32(f32), // 32-bit floating point - Char(char), // Single character - Str(String), // String (owned) - Bytes(Vec), // Raw bytes + U32(u32), // 32-bit unsigned integer + F32(f32), // 32-bit floating point + Char(char), // Single character + Str(String), // String (owned) + Bytes(Vec), // Raw bytes } pub trait SyscallHandler: std::fmt::Debug { @@ -38,7 +38,7 @@ pub trait SyscallHandler: std::fmt::Debug { &mut self, call_id: u32, args: [u32; 6], - memory: Rc>, + memory: SharedMemory, storage: Rc>, host: &mut Box, regs: &mut [u32; 32], @@ -54,7 +54,10 @@ pub struct DefaultSyscallHandler { impl std::fmt::Debug for DefaultSyscallHandler { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DefaultSyscallHandler") - .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "")) + .field( + "verbose_writer", + &self.verbose_writer.as_ref().map(|_| ""), + ) .finish() } } @@ -65,7 +68,7 @@ impl DefaultSyscallHandler { verbose_writer: None, } } - + pub fn with_writer(writer: Option>>) -> Self { Self { verbose_writer: writer, @@ -78,7 +81,7 @@ impl SyscallHandler for DefaultSyscallHandler { &mut self, call_id: u32, args: [u32; 6], - memory: Rc>, + memory: SharedMemory, storage: Rc>, host: &mut Box, regs: &mut [u32; 32], @@ -110,26 +113,31 @@ impl SyscallHandler for DefaultSyscallHandler { } impl DefaultSyscallHandler { - pub fn sys_fire_event( - &mut self, args: [u32; 6], - memory: Rc>, - host: &mut Box, - metering: &mut dyn Metering) -> u32 { + pub fn sys_fire_event( + &mut self, + args: [u32; 6], + memory: SharedMemory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { // EDUCATIONAL: Extract key pointer and length from arguments let ptr = args[0] as usize; let len = args[1] as usize; - if matches!(metering.on_syscall_data(SYSCALL_FIRE_EVENT, len), MeterResult::Halt) { + if matches!( + metering.on_syscall_data(SYSCALL_FIRE_EVENT, len), + MeterResult::Halt + ) { panic!("Metering halted SYSCALL_FIRE_EVENT"); } - let borrowed_memory = memory.borrow(); + let borrowed_memory = memory.as_ref(); // EDUCATIONAL: Safely read the key from memory // EDUCATIONAL: Create a limited scope to avoid borrow checker issues let event_bytes = match borrowed_memory.mem_slice(ptr, ptr + len) { Some(r) => r, - None => panic!("invalid memory access"), // Invalid memory access + None => panic!("invalid memory access"), // Invalid memory access }; host.fire_event(event_bytes.to_vec()); @@ -137,56 +145,74 @@ impl DefaultSyscallHandler { } fn sys_storage_get( - &mut self, - args: [u32; 6], - memory: Rc>, - storage: Rc>, - metering: &mut dyn Metering) -> u32 { + &mut self, + args: [u32; 6], + memory: SharedMemory, + storage: Rc>, + metering: &mut dyn Metering, + ) -> u32 { let domain_ptr = args[0] as usize; let domain_len = args[1] as usize; let key_ptr = args[2] as usize; let key_len = args[3] as usize; let total_len = domain_len.saturating_add(key_len); - if matches!(metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), MeterResult::Halt) { + if matches!( + metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), + MeterResult::Halt + ) { panic!("Metering halted SYSCALL_STORAGE_GET"); } - - let borrowed_memory = memory.borrow(); - + + let borrowed_memory = memory.as_ref(); + // Parse domain let domain_slice = { - let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { - Some(r) => r, - None => { - println!("❌ Storage GET - Invalid domain memory access: ptr={}, len={}", domain_ptr, domain_len); - return 0; - } - }; + let domain_slice_ref = + match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { + Some(r) => r, + None => { + println!( + "❌ Storage GET - Invalid domain memory access: ptr={}, len={}", + domain_ptr, domain_len + ); + return 0; + } + }; domain_slice_ref.as_ref().to_vec() }; let domain = match core::str::from_utf8(&domain_slice) { Ok(s) => s, Err(_) => { - println!("❌ Storage GET - Invalid UTF-8 in domain: {:?}", domain_slice); + println!( + "❌ Storage GET - Invalid UTF-8 in domain: {:?}", + domain_slice + ); return 0; } }; - + // Parse key let key_slice = { let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { Some(r) => r, None => { - println!("❌ Storage GET - Invalid key memory access: ptr={}, len={}", key_ptr, key_len); + println!( + "❌ Storage GET - Invalid key memory access: ptr={}, len={}", + key_ptr, key_len + ); return 0; } }; key_slice_ref.as_ref().to_vec() }; // Convert binary key to hex string for storage - let key = key_slice.iter().map(|b| format!("{:02x}", b)).collect::>().join(""); - + let key = key_slice + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + // Format key for display based on domain let display_key = if domain == "P" { // For persistent domain, try to display key as ASCII @@ -198,7 +224,7 @@ impl DefaultSyscallHandler { // For other domains, show domain as ASCII and key as hex format!("{}:{}", domain, key) }; - + if let Some(value) = storage.borrow().get(domain, &key) { let mut buf = (value.len() as u32).to_le_bytes().to_vec(); buf.extend_from_slice(value.as_slice()); @@ -206,20 +232,27 @@ impl DefaultSyscallHandler { panic!("Metering halted alloc during storage_get"); } let addr = borrowed_memory.alloc_on_heap(&buf); - println!("✅ Found value for domain: '{}', Key: '{}'", domain, display_key); + println!( + "✅ Found value for domain: '{}', Key: '{}'", + domain, display_key + ); return addr; } else { - println!("❌ No value found for domain: '{}', key: '{}'", domain, display_key); + println!( + "❌ No value found for domain: '{}', key: '{}'", + domain, display_key + ); 0 } } fn sys_storage_set( - &mut self, - args: [u32; 6], - memory: Rc>, - storage: Rc>, - metering: &mut dyn Metering) -> u32 { + &mut self, + args: [u32; 6], + memory: SharedMemory, + storage: Rc>, + metering: &mut dyn Metering, + ) -> u32 { let domain_ptr = args[0] as usize; let domain_len = args[1] as usize; let key_ptr = args[2] as usize; @@ -227,20 +260,25 @@ impl DefaultSyscallHandler { let val_ptr = args[4] as usize; let val_len = args[5] as usize; - let total_len = domain_len - .saturating_add(key_len) - .saturating_add(val_len); - if matches!(metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), MeterResult::Halt) { + let total_len = domain_len.saturating_add(key_len).saturating_add(val_len); + if matches!( + metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), + MeterResult::Halt + ) { panic!("Metering halted SYSCALL_STORAGE_SET"); } - - let borrowed_memory = memory.borrow(); - + + let borrowed_memory = memory.as_ref(); + // Parse domain - let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { + let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) + { Some(r) => r, None => { - println!("❌ Storage SET - Invalid domain memory access: ptr={}, len={}", domain_ptr, domain_len); + println!( + "❌ Storage SET - Invalid domain memory access: ptr={}, len={}", + domain_ptr, domain_len + ); return 0; } }; @@ -248,23 +286,33 @@ impl DefaultSyscallHandler { let domain = match core::str::from_utf8(domain_slice) { Ok(s) => s, Err(_) => { - println!("❌ Storage SET - Invalid UTF-8 in domain: {:?}", domain_slice); + println!( + "❌ Storage SET - Invalid UTF-8 in domain: {:?}", + domain_slice + ); return 0; } }; - + // Parse key let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { Some(r) => r, None => { - println!("❌ Storage SET - Invalid key memory access: ptr={}, len={}", key_ptr, key_len); + println!( + "❌ Storage SET - Invalid key memory access: ptr={}, len={}", + key_ptr, key_len + ); return 0; } }; let key_slice = key_slice_ref.as_ref(); // Convert binary key to hex string for storage - let key = key_slice.iter().map(|b| format!("{:02x}", b)).collect::>().join(""); - + let key = key_slice + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + // Format key for display based on domain let display_key = if domain == "P" { // For persistent domain, try to display key as ASCII @@ -276,54 +324,65 @@ impl DefaultSyscallHandler { // For other domains, show domain as ASCII and key as hex format!("{}:{}", domain, key) }; - + // Parse value let value_slice_ref = match borrowed_memory.mem_slice(val_ptr, val_ptr + val_len) { Some(r) => r, None => { - println!("❌ Storage SET - Invalid value memory access: ptr={}, len={}", val_ptr, val_len); + println!( + "❌ Storage SET - Invalid value memory access: ptr={}, len={}", + val_ptr, val_len + ); return 0; } }; let value_slice = value_slice_ref.as_ref(); - - println!("💾 Storage SET - Domain: '{}', Key: '{}', Value: {:?} ({} bytes)", - domain, display_key, value_slice, value_slice.len()); - + + println!( + "💾 Storage SET - Domain: '{}', Key: '{}', Value: {:?} ({} bytes)", + domain, + display_key, + value_slice, + value_slice.len() + ); + storage.borrow_mut().set(domain, &key, value_slice.to_vec()); 0 } - fn sys_panic_with_message( - &mut self, - regs: &mut [u32; 32], - memory: Rc>) -> u32 { + fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: SharedMemory) -> u32 { let msg_ptr = regs[Register::A0 as usize] as usize; let msg_len = regs[Register::A1 as usize] as usize; let msg = memory - .borrow() .mem_slice(msg_ptr, msg_ptr + msg_len) - .map(|bytes| { - String::from_utf8_lossy(&bytes).into_owned() - }) + .map(|bytes| String::from_utf8_lossy(bytes.as_ref()).into_owned()) .unwrap_or_else(|| "".to_string()); panic!("🔥 Guest panic: {}", msg); } - fn sys_log(&mut self, args: [u32; 6], memory: Rc>, metering: &mut dyn Metering) -> u32 { + fn sys_log( + &mut self, + args: [u32; 6], + memory: SharedMemory, + metering: &mut dyn Metering, + ) -> u32 { let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; let payload_len = fmt_len.saturating_add(arg_len) as usize; - if matches!(metering.on_syscall_data(SYSCALL_LOG, payload_len), MeterResult::Halt) { + if matches!( + metering.on_syscall_data(SYSCALL_LOG, payload_len), + MeterResult::Halt + ) { panic!("Metering halted SYSCALL_LOG"); } - let borrowed_memory = memory.borrow(); - let fmt_slice = match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { - Some(s) => s, - None => { - println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); - return 0; - } - }; + let borrowed_memory = memory.as_ref(); + let fmt_slice = + match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { + Some(s) => s, + None => { + println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); + return 0; + } + }; let fmt_bytes = fmt_slice.as_ref(); let fmt = match core::str::from_utf8(fmt_bytes) { Ok(s) => s, @@ -334,7 +393,8 @@ impl DefaultSyscallHandler { return 0; } }; - let args_bytes_slice = borrowed_memory.mem_slice(arg_ptr as usize, (arg_ptr + arg_len) as usize); + let args_bytes_slice = + borrowed_memory.mem_slice(arg_ptr as usize, (arg_ptr + arg_len) as usize); let args_bytes_holder; let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { args_bytes_holder = slice; @@ -350,7 +410,9 @@ impl DefaultSyscallHandler { let mut raw_iter = raw_args.into_iter(); let mut chars = fmt.chars().peekable(); while let Some(c) = chars.next() { - if c != '%' { continue; } + if c != '%' { + continue; + } let spec: char = chars.next().unwrap_or('%'); let mut next = || raw_iter.next().unwrap_or(0); match spec { @@ -400,7 +462,7 @@ impl DefaultSyscallHandler { } } 'A' => { - // Array of u8s + // Array of u8s let ptr = next() as usize; let len = next() as usize; match borrowed_memory.mem_slice(ptr, ptr + len) { @@ -446,11 +508,13 @@ impl DefaultSyscallHandler { // Format bytes array nicely output.push('['); for (i, byte) in b.iter().enumerate() { - if i > 0 { output.push_str(", "); } + if i > 0 { + output.push_str(", "); + } output.push_str(&format!("0x{:02x}", byte)); } output.push(']'); - }, + } _ => output.push_str(""), }, Some('a') => match args_iter.next() { @@ -458,24 +522,29 @@ impl DefaultSyscallHandler { // Format u32 array (bytes interpreted as u32s) output.push('['); for (i, chunk) in b.chunks_exact(4).enumerate() { - if i > 0 { output.push_str(", "); } - let val = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + if i > 0 { + output.push_str(", "); + } + let val = + u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); output.push_str(&format!("{}", val)); } output.push(']'); - }, + } _ => output.push_str(""), }, Some('A') => match args_iter.next() { Some(Arg::Bytes(b)) => { - // Format u8 array + // Format u8 array output.push('['); for (i, byte) in b.iter().enumerate() { - if i > 0 { output.push_str(", "); } + if i > 0 { + output.push_str(", "); + } output.push_str(&format!("{}", byte)); } output.push(']'); - }, + } _ => output.push_str(""), }, Some('%') => output.push('%'), @@ -496,7 +565,13 @@ impl DefaultSyscallHandler { 0 } - fn sys_call_program(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box, metering: &mut dyn Metering) -> u32 { + fn sys_call_program( + &mut self, + args: [u32; 6], + memory: SharedMemory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { let to_ptr = args[0] as usize; let from_ptr = args[1] as usize; let input_ptr = args[2] as usize; @@ -507,7 +582,7 @@ impl DefaultSyscallHandler { panic!("Metering halted SYSCALL_CALL_PROGRAM"); } { - let borrowed_memory = memory.borrow(); + let borrowed_memory = memory.as_ref(); let to_slice = match borrowed_memory.mem_slice(to_ptr, to_ptr + 20) { Some(r) => r, None => return 0, @@ -528,7 +603,7 @@ impl DefaultSyscallHandler { (result_ptr, page_index) = host.call_program(from_bytes, to_bytes, input_vec); } { - let borrowed_memory = memory.borrow_mut(); + let borrowed_memory = memory.as_ref(); let result_bytes = match host.read_memory_page(page_index, result_ptr, RESULT_SIZE) { Some(b) => b, None => return 0, @@ -540,60 +615,73 @@ impl DefaultSyscallHandler { } } - fn sys_alloc(&mut self, args: [u32; 6], memory: Rc>, metering: &mut dyn Metering) -> u32 { - let size = args[0] as usize; // A0 register + fn sys_alloc( + &mut self, + args: [u32; 6], + memory: SharedMemory, + metering: &mut dyn Metering, + ) -> u32 { + let size = args[0] as usize; // A0 register let align = args[1] as usize; // A1 register if matches!(metering.on_alloc(size), MeterResult::Halt) { panic!("Metering halted SYSCALL_ALLOC"); } - + if size == 0 { println!("VM Alloc: Invalid size 0"); return 0; } - + // Validate alignment (must be power of 2) if align == 0 || (align & (align - 1)) != 0 { println!("VM Alloc: Invalid alignment {}", align); return 0; } - - let current_heap = memory.borrow().next_heap.get(); - + + let current_heap = memory.next_heap(); + // Initialize heap pointer if not set (no code has been written) if current_heap == 0 { - memory.borrow().next_heap.set(HEAP_PTR_OFFSET); + memory.set_next_heap(HEAP_PTR_OFFSET); } - + // Allocate aligned memory on heap let data = vec![0u8; size]; - let ptr = memory.borrow().alloc_on_heap(&data); - + let ptr = memory.alloc_on_heap(&data); + if ptr == 0 { println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); return 0; } - + // Check if allocated address meets alignment requirements if (ptr as usize) % align != 0 { // Re-allocate with enough space for alignment let total_size = size + align - 1; let padded_data = vec![0u8; total_size]; - let padded_ptr = memory.borrow().alloc_on_heap(&padded_data); + let padded_ptr = memory.alloc_on_heap(&padded_data); if padded_ptr == 0 { - println!("VM Alloc: Out of memory, failed to allocate {} bytes for alignment", total_size); + println!( + "VM Alloc: Out of memory, failed to allocate {} bytes for alignment", + total_size + ); return 0; } // Return properly aligned pointer within the allocated region let aligned_ptr = ((padded_ptr as usize + align - 1) & !(align - 1)) as u32; return aligned_ptr; } - + ptr } - - fn sys_dealloc(&mut self, args: [u32; 6], _memory: Rc>, metering: &mut dyn Metering) -> u32 { + + fn sys_dealloc( + &mut self, + args: [u32; 6], + _memory: SharedMemory, + metering: &mut dyn Metering, + ) -> u32 { let size = args[1] as usize; if matches!(metering.on_alloc(size), MeterResult::Halt) { panic!("Metering halted SYSCALL_DEALLOC"); @@ -604,41 +692,67 @@ impl DefaultSyscallHandler { 0 } - fn sys_transfer(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box, metering: &mut dyn Metering) -> u32 { + fn sys_transfer( + &mut self, + args: [u32; 6], + memory: SharedMemory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { // args: a2=to ptr, a3=value_lo, a4=value_hi let to_ptr = args[1] as usize; let value_lo = args[2] as u64; let value_hi = args[3] as u64; let value = value_lo | (value_hi << 32); - if matches!(metering.on_syscall_data(SYSCALL_TRANSFER, 20), MeterResult::Halt) { + if matches!( + metering.on_syscall_data(SYSCALL_TRANSFER, 20), + MeterResult::Halt + ) { panic!("Metering halted SYSCALL_TRANSFER"); } - let borrowed = memory.borrow(); - let to_slice = borrowed.mem_slice(to_ptr, to_ptr + 20).expect("invalid to ptr"); + let borrowed = memory.as_ref(); + let to_slice = borrowed + .mem_slice(to_ptr, to_ptr + 20) + .expect("invalid to ptr"); let mut to = [0u8; 20]; to.copy_from_slice(to_slice.as_ref()); - if host.transfer(to, value) { 0 } else { 1 } + if host.transfer(to, value) { + 0 + } else { + 1 + } } - fn sys_balance(&mut self, args: [u32; 6], memory: Rc>, host: &mut Box, metering: &mut dyn Metering) -> u32 { + fn sys_balance( + &mut self, + args: [u32; 6], + memory: SharedMemory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { // args: a1 = address pointer (20 bytes) let addr_ptr = args[0] as usize; - if matches!(metering.on_syscall_data(SYSCALL_BALANCE, 20), MeterResult::Halt) { + if matches!( + metering.on_syscall_data(SYSCALL_BALANCE, 20), + MeterResult::Halt + ) { panic!("Metering halted SYSCALL_BALANCE"); } let addr = { - let borrowed = memory.borrow(); - let addr_slice = borrowed.mem_slice(addr_ptr, addr_ptr + 20).expect("invalid addr ptr"); + let borrowed = memory.as_ref(); + let addr_slice = borrowed + .mem_slice(addr_ptr, addr_ptr + 20) + .expect("invalid addr ptr"); let mut addr = [0u8; 20]; addr.copy_from_slice(addr_slice.as_ref()); addr }; let bal = host.balance(addr); - memory.borrow().alloc_on_heap(&bal.to_le_bytes()) + memory.alloc_on_heap(&bal.to_le_bytes()) } } diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 5db6b77..e9bf0f9 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,36 +1,36 @@ -use std::rc::Rc; -use core::cell::RefCell; use crate::cpu::CPU; -use crate::registers::Register; -use crate::memory_page::{MemoryPage}; -use storage::{Storage}; use crate::host_interface::HostInterface; -use crate::sys_call::{SyscallHandler, DefaultSyscallHandler}; +use crate::memory::SharedMemory; use crate::metering::Metering; +use crate::registers::Register; +use crate::sys_call::{DefaultSyscallHandler, SyscallHandler}; +use core::cell::RefCell; +use std::rc::Rc; +use storage::Storage; /// Represents a complete RISC-V virtual machine. -/// +/// /// EDUCATIONAL PURPOSE: This struct encapsulates all the components needed /// to run a virtual machine: CPU, memory, and persistent storage. It provides /// a high-level interface for VM operations while hiding the complexity of /// the underlying components. -/// +/// /// VM ARCHITECTURE OVERVIEW: /// - CPU: Executes RISC-V instructions /// - Memory: Provides RAM for the running program /// - Storage: Persistent storage for data that survives between runs -/// -/// MEMORY MANAGEMENT: Uses Rc> for shared mutable access to memory -/// and storage, allowing the VM to manage resources efficiently while maintaining -/// Rust's safety guarantees. +/// +/// MEMORY MANAGEMENT: Uses Rc to share a trait-backed memory implementation +/// and Rc> for persistent storage, allowing the VM to manage +/// resources efficiently while maintaining Rust's safety guarantees. #[derive(Debug)] pub struct VM { /// The CPU that executes RISC-V instructions pub cpu: CPU, - - /// Shared reference to the VM's memo ry (RAM) - pub memory: Rc>, - + + /// Shared reference to the VM's memory (RAM) + pub memory: SharedMemory, + /// Shared reference to persistent storage pub storage: Rc>, @@ -40,34 +40,49 @@ pub struct VM { impl VM { /// Creates a new virtual machine with the specified memory, storage, and host, using the default syscall handler. pub fn new( - memory: Rc>, - storage: Rc>, + memory: SharedMemory, + storage: Rc>, host: Box, ) -> Self { - Self::new_with_syscall_handler(memory, storage, host, Box::new(DefaultSyscallHandler::new())) + Self::new_with_syscall_handler( + memory, + storage, + host, + Box::new(DefaultSyscallHandler::new()), + ) } - + /// Creates a new virtual machine with a writer for logging output. pub fn new_with_writer( - memory: Rc>, - storage: Rc>, + memory: SharedMemory, + storage: Rc>, host: Box, writer: Option>>, ) -> Self { - Self::new_with_syscall_handler(memory, storage, host, Box::new(DefaultSyscallHandler::with_writer(writer))) + Self::new_with_syscall_handler( + memory, + storage, + host, + Box::new(DefaultSyscallHandler::with_writer(writer)), + ) } /// Creates a new virtual machine with a custom syscall handler. /// This is useful for testing or custom environments. pub fn new_with_syscall_handler( - memory: Rc>, - storage: Rc>, + memory: SharedMemory, + storage: Rc>, host: Box, syscall_handler: Box, ) -> Self { let mut cpu = CPU::new(syscall_handler); - cpu.regs[Register::Sp as usize] = memory.borrow().stack_top(); - Self { cpu, memory, storage, host } + cpu.regs[Register::Sp as usize] = memory.stack_top(); + Self { + cpu, + memory, + storage, + host, + } } /// Installs a metering implementation on the underlying CPU. @@ -76,56 +91,56 @@ impl VM { } /// Loads program code into memory and sets the starting address. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates how programs are loaded into /// a VM. In real systems, this would involve loading from disk, parsing /// executable formats, and setting up memory protection. - /// + /// /// PARAMETERS: /// - alloc_add: The address where the code should be allocated in memory /// - start_addr: Where the program should start executing from /// - code: The binary program code to load - /// + /// /// MEMORY LAYOUT: Programs are typically loaded at specific addresses /// to ensure proper alignment and to avoid conflicts with system memory. pub fn set_code(&mut self, alloc_add: u32, start_addr: u32, code: &[u8]) { // EDUCATIONAL: Write the program code to memory starting at address 0 - self.memory.borrow_mut().write_code(alloc_add as usize, code); - + self.memory.write_code(alloc_add as usize, code); + // EDUCATIONAL: Set the program counter to the starting address self.cpu.pc = start_addr; } /// Allocates memory on the heap and writes data to it. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates dynamic memory allocation in a VM. /// Programs need to allocate memory for variables, arrays, and other data /// structures at runtime. - /// + /// /// HEAP MANAGEMENT: The VM maintains a heap pointer that moves forward /// as memory is allocated. This is a simple but effective allocation strategy. - /// + /// /// RETURN VALUE: Returns the address where the data was written pub fn alloc_and_write(&mut self, data: &[u8]) -> u32 { - self.memory.borrow_mut().alloc_on_heap(data) + self.memory.alloc_on_heap(data) } /// Sets a register to point to data in memory. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates how to pass data to programs /// running in the VM. Instead of copying data into registers (which are /// limited in size), we store the data in memory and pass the address. - /// + /// /// PARAMETER PASSING: This is how we pass strings, arrays, and other /// large data structures to programs. The register contains a pointer /// to the actual data in memory. - /// + /// /// DEBUG OUTPUT: The function prints information about what it's doing, /// which is helpful for understanding VM behavior during development. pub fn set_reg_to_data(&mut self, reg: Register, data: &[u8]) -> u32 { // EDUCATIONAL: Allocate memory and write the data let addr = self.alloc_and_write(data); - + // EDUCATIONAL: Set the register to point to the data self.cpu.regs[reg as usize] = addr; @@ -136,20 +151,16 @@ impl VM { addr, data.len() ); - println!( - "📦 data written to 0x{:08x}: {:02x?}", - addr, - data - ); + println!("📦 data written to 0x{:08x}: {:02x?}", addr, data); addr } /// Sets a register to a 32-bit value. - /// + /// /// EDUCATIONAL PURPOSE: This is used for passing small values (like /// integers) directly to programs. For larger data, use set_reg_to_data. - /// + /// /// USAGE: Typically used for passing function parameters, flags, or /// other small values that fit in a single register. pub fn set_reg_u32(&mut self, reg: Register, data: u32) { @@ -157,40 +168,40 @@ impl VM { } /// Dumps the entire memory contents for debugging. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates memory inspection tools that /// are essential for debugging VM programs. It shows both hex and ASCII /// representations of memory contents. - /// + /// /// DEBUGGING: Memory dumps are crucial for understanding what's happening /// when programs don't work as expected. They show the actual data in memory. pub fn dump_all_memory(&self) { - self.dump_memory(0, self.memory.borrow().mem().len()); + self.dump_memory(0, self.memory.mem().len()); } /// Dumps a specific range of memory for debugging. - /// + /// /// EDUCATIONAL PURPOSE: This function provides a detailed view of memory /// contents, showing both hexadecimal and ASCII representations. This is /// similar to tools like 'hexdump' or 'xxd' in Unix systems. - /// + /// /// OUTPUT FORMAT: /// - Address in hexadecimal /// - 16 bytes of data in hex format /// - ASCII representation (printable characters only) /// - Heap pointer location - /// + /// /// MEMORY LAYOUT: The output shows how memory is organized, including /// where the heap pointer is and what data is stored where. pub fn dump_memory(&self, start: usize, end: usize) { - let borrowed_memory = self.memory.borrow(); + let borrowed_memory = self.memory.as_ref(); // EDUCATIONAL: Validate memory range to prevent errors assert!(start < end, "invalid memory range"); assert!(end <= borrowed_memory.mem().len(), "range out of bounds"); // EDUCATIONAL: Show heap pointer for context - let next_heap = borrowed_memory.next_heap.get(); + let next_heap = borrowed_memory.next_heap(); println!("--- Memory Dump ---"); println!("Next heap pointer: 0x{:08x}", next_heap); @@ -203,7 +214,8 @@ impl VM { let hex_str = hex.join(" "); // EDUCATIONAL: Convert bytes to ASCII (printable characters only) - let ascii: String = line.iter() + let ascii: String = line + .iter() .map(|&b| if b.is_ascii_graphic() { b as char } else { '.' }) .collect(); @@ -213,14 +225,14 @@ impl VM { } /// Dumps the current state of all CPU registers for debugging. - /// + /// /// EDUCATIONAL PURPOSE: This demonstrates register inspection, which is /// essential for understanding program state and debugging issues. - /// + /// /// RISC-V REGISTER CONVENTIONS: The output shows both register numbers /// and their ABI (Application Binary Interface) names, which helps /// understand how registers are used in RISC-V programs. - /// + /// /// REGISTER USAGE: /// - x0 (zero): Always zero /// - x1 (ra): Return address @@ -233,10 +245,9 @@ impl VM { // EDUCATIONAL: RISC-V ABI register names for easier understanding const ABI_NAMES: [&str; 32] = [ - "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", - "s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5", - "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", - "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6", + "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3", + "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", + "t3", "t4", "t5", "t6", ]; // EDUCATIONAL: Display each register with its name and value @@ -252,20 +263,24 @@ impl VM { } /// Starts program execution without initializing registers or setting up state. - /// + /// /// EDUCATIONAL PURPOSE: This is the main execution loop of the VM. It /// continuously fetches, decodes, and executes instructions until the /// program halts or encounters an error. - /// + /// /// EXECUTION LOOP: This implements the classic fetch-decode-execute cycle /// that all CPUs follow. The loop continues until the CPU returns false, /// indicating that execution should stop. - /// + /// /// ASSUMPTIONS: This function assumes the VM is already properly configured /// with code loaded and registers set up. For a complete VM, you'd typically /// call this after setting up the initial state. pub fn raw_run(&mut self) { // EDUCATIONAL: Main execution loop - fetch, decode, execute - while self.cpu.step(Rc::clone(&self.memory), Rc::clone(&self.storage), &mut self.host) {} + while self.cpu.step( + Rc::clone(&self.memory), + Rc::clone(&self.storage), + &mut self.host, + ) {} } -} +} From a32f60404b12f839afc3db53454893e855c95ae5 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 12 Dec 2025 23:04:04 +0200 Subject: [PATCH 05/70] Rename memory trait and shared alias --- crates/avm/src/memory/memory_page.rs | 4 +-- crates/avm/src/memory/memory_page_manager.rs | 14 +++++------ crates/avm/tests/allocator_test.rs | 10 ++++---- crates/avm/tests/spec_runner.rs | 4 +-- crates/avm/tests/test_syscall_handler.rs | 8 +++--- crates/vm/src/cpu.rs | 10 ++++---- crates/vm/src/exe.rs | 4 +-- crates/vm/src/memory.rs | 4 +-- crates/vm/src/sys_call.rs | 26 ++++++++++---------- crates/vm/src/vm.rs | 10 ++++---- 10 files changed, 47 insertions(+), 47 deletions(-) diff --git a/crates/avm/src/memory/memory_page.rs b/crates/avm/src/memory/memory_page.rs index d453bad..318f6f1 100644 --- a/crates/avm/src/memory/memory_page.rs +++ b/crates/avm/src/memory/memory_page.rs @@ -1,7 +1,7 @@ use std::cell::{Cell, Ref, RefCell}; use std::convert::TryInto; use std::rc::Rc; -use vm::memory::{Memory, HEAP_PTR_OFFSET}; +use vm::memory::{memory, HEAP_PTR_OFFSET}; use vm::metering::{MeterResult, Metering, MemoryAccessKind}; #[derive(Debug, Clone)] @@ -225,7 +225,7 @@ impl Default for MemoryPage { } } -impl Memory for MemoryPage { +impl memory for MemoryPage { fn mem(&self) -> Ref> { MemoryPage::mem(self) } diff --git a/crates/avm/src/memory/memory_page_manager.rs b/crates/avm/src/memory/memory_page_manager.rs index 77bbc9f..caf1467 100644 --- a/crates/avm/src/memory/memory_page_manager.rs +++ b/crates/avm/src/memory/memory_page_manager.rs @@ -1,12 +1,12 @@ use std::rc::Rc; use crate::memory::MemoryPage; -use vm::memory::SharedMemory; +use vm::memory::Memory; #[derive(Debug)] pub struct MemoryPageManager { pub page_size: usize, max_pages: usize, - pages: Vec, + pages: Vec, } impl MemoryPageManager { @@ -22,7 +22,7 @@ impl MemoryPageManager { } /// Creates and owns a new page. Returns a mutable reference to it. - pub fn new_page(&mut self) -> SharedMemory { + pub fn new_page(&mut self) -> Memory { if self.pages.len() >= self.max_pages { panic!( "Out of memory: maximum page count ({}) reached", @@ -30,7 +30,7 @@ impl MemoryPageManager { ); } - let page: SharedMemory = Rc::new(MemoryPage::new(self.page_size)); + let page: Memory = Rc::new(MemoryPage::new(self.page_size)); self.pages.push(Rc::clone(&page)); return page; } @@ -76,15 +76,15 @@ impl MemoryPageManager { } } - pub fn get_page(&self, index: usize) -> Option { + pub fn get_page(&self, index: usize) -> Option { self.pages.get(index).cloned() // ✅ clone the Rc (increases refcount) } - pub fn first_page(&self) -> Option { + pub fn first_page(&self) -> Option { self.pages.first().cloned() // ✅ clone the Rc (increases refcount) } - pub fn top_page(&self) -> Option { + pub fn top_page(&self) -> Option { self.pages.last().cloned() // ✅ clone the Rc (increases refcount) } } diff --git a/crates/avm/tests/allocator_test.rs b/crates/avm/tests/allocator_test.rs index 308b766..4bbd732 100644 --- a/crates/avm/tests/allocator_test.rs +++ b/crates/avm/tests/allocator_test.rs @@ -4,12 +4,12 @@ use std::rc::Rc; use storage::Storage; use vm::host_interface; use vm::metering::NoopMeter; -use vm::memory::SharedMemory; +use vm::memory::Memory; use vm::sys_call::{DefaultSyscallHandler, SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { - let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); @@ -47,7 +47,7 @@ fn test_allocator_syscalls() { #[test] fn test_multiple_allocations() { - let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); @@ -90,7 +90,7 @@ fn test_multiple_allocations() { #[test] fn test_alignment_requirements() { - let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); @@ -119,7 +119,7 @@ fn test_alignment_requirements() { #[test] fn test_invalid_alignment() { - let memory: SharedMemory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(MemoryPage::new(8192)); let storage = Rc::new(RefCell::new(Storage::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(); diff --git a/crates/avm/tests/spec_runner.rs b/crates/avm/tests/spec_runner.rs index e9a212d..79323c6 100644 --- a/crates/avm/tests/spec_runner.rs +++ b/crates/avm/tests/spec_runner.rs @@ -7,7 +7,7 @@ use avm::memory::MemoryPage; use vm::vm::VM; mod test_syscall_handler; use test_syscall_handler::TestSyscallHandler; -use vm::memory::SharedMemory; +use vm::memory::Memory; /// Tests that are skipped and the reasons why const SKIPPED_TESTS: &[(&str, &str)] = &[ @@ -79,7 +79,7 @@ fn run_single_test(elf_path: &str) -> Result<(), Box> { } // Set up VM memory (allocate enough to cover 0x80000000+) - let memory: SharedMemory = std::rc::Rc::new(MemoryPage::new_with_base( + let memory: Memory = std::rc::Rc::new(MemoryPage::new_with_base( 0x20000, 0x80000000, )); // 128KB at 0x80000000 diff --git a/crates/avm/tests/test_syscall_handler.rs b/crates/avm/tests/test_syscall_handler.rs index 7248ddd..2ac4e31 100644 --- a/crates/avm/tests/test_syscall_handler.rs +++ b/crates/avm/tests/test_syscall_handler.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use storage::Storage; use vm::host_interface::HostInterface; use vm::metering::Metering; -use vm::memory::SharedMemory; +use vm::memory::Memory; use vm::registers::Register; use vm::sys_call::SyscallHandler; @@ -24,7 +24,7 @@ fn exit_code_to_test_num(exit_code: u32) -> Option { #[derive(Debug)] pub struct TestSyscallHandler { tohost_addr: u64, - memory: Option, + memory: Option, } impl TestSyscallHandler { @@ -41,7 +41,7 @@ impl TestSyscallHandler { } /// Set the memory reference (needed to read .tohost) - pub fn set_memory(&mut self, memory: SharedMemory) { + pub fn set_memory(&mut self, memory: Memory) { self.memory = Some(memory); } } @@ -54,7 +54,7 @@ impl SyscallHandler for TestSyscallHandler { &mut self, call_id: u32, _args: [u32; 6], - memory: SharedMemory, + memory: Memory, _storage: Rc>, _host: &mut Box, regs: &mut [u32; 32], diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 3593161..d418855 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -1,7 +1,7 @@ use crate::decoder::{decode_compressed, decode_full}; use crate::host_interface::HostInterface; use crate::instruction::Instruction; -use crate::memory::SharedMemory; +use crate::memory::Memory; use crate::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; use crate::sys_call::SyscallHandler; use core::cell::RefCell; @@ -213,7 +213,7 @@ impl CPU { /// redirects the flow (like a branch or jump instruction). pub fn step( &mut self, - memory: SharedMemory, + memory: Memory, storage: Rc>, host: &mut Box, ) -> bool { @@ -251,7 +251,7 @@ impl CPU { &mut self, instr: Instruction, size: u8, - memory: SharedMemory, + memory: Memory, storage: Rc>, host: &mut Box, ) -> bool { @@ -311,7 +311,7 @@ impl CPU { /// RETURN VALUE: Returns false to halt execution on invalid instructions fn unknown_instruction( &mut self, - memory: SharedMemory, + memory: Memory, _storage: Rc>, ) -> bool { // EDUCATIONAL: Try to read the invalid instruction bytes for debugging @@ -347,7 +347,7 @@ impl CPU { /// an instruction is compressed (not 0b11) or regular (0b11). /// /// RETURN VALUE: Returns Some((instruction, size)) if successful, None if invalid - pub fn next_instruction(&mut self, memory: SharedMemory) -> Option<(Instruction, u8)> { + pub fn next_instruction(&mut self, memory: Memory) -> Option<(Instruction, u8)> { let pc = self.pc as usize; // EDUCATIONAL: Read 4 bytes from memory (enough for any instruction) diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index 1adb838..bb96b5d 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,7 +1,7 @@ use core::cell::RefCell; use std::rc::Rc; -use super::{Instruction, MemoryAccessKind, SharedMemory, CPU}; +use super::{Instruction, MemoryAccessKind, Memory, CPU}; use crate::host_interface::HostInterface; use crate::instruction::CsrOp; use crate::registers::Register; @@ -30,7 +30,7 @@ impl CPU { pub fn execute( &mut self, instr: Instruction, - memory: SharedMemory, + memory: Memory, storage: Rc>, host: &mut Box, ) -> bool { diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory.rs index 424b0f6..cd8f3d9 100644 --- a/crates/vm/src/memory.rs +++ b/crates/vm/src/memory.rs @@ -4,7 +4,7 @@ use crate::metering::{Metering, MemoryAccessKind}; pub const HEAP_PTR_OFFSET: u32 = 0x100; -pub trait Memory: std::fmt::Debug { +pub trait memory: std::fmt::Debug { fn mem(&self) -> Ref>; fn mem_slice(&self, start: usize, end: usize) -> Option>; fn store_u16(&self, addr: usize, val: u16, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; @@ -23,4 +23,4 @@ pub trait Memory: std::fmt::Debug { fn set_next_heap(&self, next: u32); } -pub type SharedMemory = Rc; +pub type Memory = Rc; diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index 9f3488d..d5ae6f0 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -1,5 +1,5 @@ use crate::host_interface::HostInterface; -use crate::memory::{SharedMemory, HEAP_PTR_OFFSET}; +use crate::memory::{Memory, HEAP_PTR_OFFSET}; use crate::metering::{MeterResult, Metering}; use crate::registers::Register; use core::cell::RefCell; @@ -38,7 +38,7 @@ pub trait SyscallHandler: std::fmt::Debug { &mut self, call_id: u32, args: [u32; 6], - memory: SharedMemory, + memory: Memory, storage: Rc>, host: &mut Box, regs: &mut [u32; 32], @@ -81,7 +81,7 @@ impl SyscallHandler for DefaultSyscallHandler { &mut self, call_id: u32, args: [u32; 6], - memory: SharedMemory, + memory: Memory, storage: Rc>, host: &mut Box, regs: &mut [u32; 32], @@ -116,7 +116,7 @@ impl DefaultSyscallHandler { pub fn sys_fire_event( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, host: &mut Box, metering: &mut dyn Metering, ) -> u32 { @@ -147,7 +147,7 @@ impl DefaultSyscallHandler { fn sys_storage_get( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, storage: Rc>, metering: &mut dyn Metering, ) -> u32 { @@ -249,7 +249,7 @@ impl DefaultSyscallHandler { fn sys_storage_set( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, storage: Rc>, metering: &mut dyn Metering, ) -> u32 { @@ -350,7 +350,7 @@ impl DefaultSyscallHandler { 0 } - fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: SharedMemory) -> u32 { + fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Memory) -> u32 { let msg_ptr = regs[Register::A0 as usize] as usize; let msg_len = regs[Register::A1 as usize] as usize; let msg = memory @@ -363,7 +363,7 @@ impl DefaultSyscallHandler { fn sys_log( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, metering: &mut dyn Metering, ) -> u32 { let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; @@ -568,7 +568,7 @@ impl DefaultSyscallHandler { fn sys_call_program( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, host: &mut Box, metering: &mut dyn Metering, ) -> u32 { @@ -618,7 +618,7 @@ impl DefaultSyscallHandler { fn sys_alloc( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, metering: &mut dyn Metering, ) -> u32 { let size = args[0] as usize; // A0 register @@ -679,7 +679,7 @@ impl DefaultSyscallHandler { fn sys_dealloc( &mut self, args: [u32; 6], - _memory: SharedMemory, + _memory: Memory, metering: &mut dyn Metering, ) -> u32 { let size = args[1] as usize; @@ -695,7 +695,7 @@ impl DefaultSyscallHandler { fn sys_transfer( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, host: &mut Box, metering: &mut dyn Metering, ) -> u32 { @@ -730,7 +730,7 @@ impl DefaultSyscallHandler { fn sys_balance( &mut self, args: [u32; 6], - memory: SharedMemory, + memory: Memory, host: &mut Box, metering: &mut dyn Metering, ) -> u32 { diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index e9bf0f9..dbf3f2a 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,6 +1,6 @@ use crate::cpu::CPU; use crate::host_interface::HostInterface; -use crate::memory::SharedMemory; +use crate::memory::Memory; use crate::metering::Metering; use crate::registers::Register; use crate::sys_call::{DefaultSyscallHandler, SyscallHandler}; @@ -29,7 +29,7 @@ pub struct VM { pub cpu: CPU, /// Shared reference to the VM's memory (RAM) - pub memory: SharedMemory, + pub memory: Memory, /// Shared reference to persistent storage pub storage: Rc>, @@ -40,7 +40,7 @@ pub struct VM { impl VM { /// Creates a new virtual machine with the specified memory, storage, and host, using the default syscall handler. pub fn new( - memory: SharedMemory, + memory: Memory, storage: Rc>, host: Box, ) -> Self { @@ -54,7 +54,7 @@ impl VM { /// Creates a new virtual machine with a writer for logging output. pub fn new_with_writer( - memory: SharedMemory, + memory: Memory, storage: Rc>, host: Box, writer: Option>>, @@ -70,7 +70,7 @@ impl VM { /// Creates a new virtual machine with a custom syscall handler. /// This is useful for testing or custom environments. pub fn new_with_syscall_handler( - memory: SharedMemory, + memory: Memory, storage: Rc>, host: Box, syscall_handler: Box, From ec867137c8a6bfe0463cc6c5d6b31bec1165fde7 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 14 Dec 2025 08:39:41 +0100 Subject: [PATCH 06/70] kernel and bootloader scaffolding --- Cargo.lock | 15 +- Cargo.toml | 2 +- crates/aos/Cargo.toml | 7 - crates/aos/src/lib.rs | 133 ---------- crates/os/Cargo.toml | 12 + crates/{aos => os}/README.md | 6 +- crates/os/src/bin/bootloader_runner.rs | 26 ++ crates/os/src/bootloader.rs | 153 ++++++++++++ crates/os/src/kernel.rs | 78 ++++++ crates/os/src/lib.rs | 14 ++ crates/os/src/memory/memory_page.rs | 330 +++++++++++++++++++++++++ crates/os/src/memory/mod.rs | 7 + crates/os/src/memory/stacked_memory.rs | 55 +++++ crates/os/src/traps.rs | 67 +++++ 14 files changed, 757 insertions(+), 148 deletions(-) delete mode 100644 crates/aos/Cargo.toml delete mode 100644 crates/aos/src/lib.rs create mode 100644 crates/os/Cargo.toml rename crates/{aos => os}/README.md (90%) create mode 100644 crates/os/src/bin/bootloader_runner.rs create mode 100644 crates/os/src/bootloader.rs create mode 100644 crates/os/src/kernel.rs create mode 100644 crates/os/src/lib.rs create mode 100644 crates/os/src/memory/memory_page.rs create mode 100644 crates/os/src/memory/mod.rs create mode 100644 crates/os/src/memory/stacked_memory.rs create mode 100644 crates/os/src/traps.rs diff --git a/Cargo.lock b/Cargo.lock index cebf0f4..1dbcc29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aos" -version = "0.1.0" - [[package]] name = "avm" version = "0.1.0" @@ -259,6 +255,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "os" +version = "0.1.0" +dependencies = [ + "avm", + "goblin", + "storage", + "types", + "vm", +] + [[package]] name = "pkcs8" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index bee76cb..d39f2a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "crates/avm", "crates/compiler", "crates/examples", - "crates/aos", + "crates/os", "crates/program", "crates/state", "crates/storage", diff --git a/crates/aos/Cargo.toml b/crates/aos/Cargo.toml deleted file mode 100644 index 10a4d41..0000000 --- a/crates/aos/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "aos" -version = "0.1.0" -edition = "2024" -readme = "README.md" - -[dependencies] diff --git a/crates/aos/src/lib.rs b/crates/aos/src/lib.rs deleted file mode 100644 index 2c5f30b..0000000 --- a/crates/aos/src/lib.rs +++ /dev/null @@ -1,133 +0,0 @@ -#![forbid(unsafe_code)] -//! Alon's OS (aOS): a deterministic, blockchain-first operating system. -//! -//! This crate hosts the top-level types and module layout for aOS. It will grow to -//! replace the current `avm` orchestrator while internalizing the `program` crate as -//! the `liba` standard library for applications. - -/// Marker type for the OS surface. It will eventually own the boot/kernel/runtime -/// wiring that `avm` performs today. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct Aos; - -/// Bootloader-focused types used to validate and hand off control to the kernel. -pub mod bootloader { - /// Build-time configuration the bootloader uses while measuring the kernel and - /// standard library images. - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct BootConfig { - /// Whether the bootloader should emit early console logs. - pub debug_console: bool, - /// Whether the bootloader may proceed with unsigned developer images. - pub allow_developer_images: bool, - } - - impl Default for BootConfig { - fn default() -> Self { - Self { - debug_console: true, - allow_developer_images: false, - } - } - } - - /// Minimal manifest created by the bootloader and consumed by the kernel. - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct BootInfo<'a> { - /// Verified kernel image payload. - pub kernel_image: &'a [u8], - /// Verified `liba` (standard library) image payload. - pub liba_image: &'a [u8], - /// Boot-time options that tailor kernel behavior. - pub config: BootConfig, - } - - impl<'a> BootInfo<'a> { - /// Creates a new manifest describing the verified runtime artifacts. - pub fn new(kernel_image: &'a [u8], liba_image: &'a [u8], config: BootConfig) -> Self { - Self { - kernel_image, - liba_image, - config, - } - } - } -} - -/// Kernel-facing structures for deterministic, capability-scoped execution. -pub mod kernel { - /// Configuration that controls which services the kernel exposes to runtimes. - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct KernelConfig { - /// Whether to emit receipts and event logs. - pub receipts_enabled: bool, - /// Whether to allow cross-program calls. - pub cross_program_calls_enabled: bool, - /// Whether to allow host crypto/syscall helpers. - pub host_functions_enabled: bool, - } - - impl Default for KernelConfig { - fn default() -> Self { - Self { - receipts_enabled: true, - cross_program_calls_enabled: true, - host_functions_enabled: true, - } - } - } - - /// Placeholder for the kernel instance that will own scheduling and capability setup. - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] - pub struct Kernel; -} - -/// Runtime-level types that coordinate block execution and VM orchestration. -pub mod runtime { - /// Execution context provided to each transaction/program. - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct ExecutionContext { - /// Height or slot of the block being executed. - pub height: u64, - /// Hash of the parent state root for reproducibility. - pub parent_state_root: [u8; 32], - /// Index of the transaction inside the block. - pub tx_index: u32, - } - - impl ExecutionContext { - /// Builds a new execution context for a transaction. - pub fn new(height: u64, parent_state_root: [u8; 32], tx_index: u32) -> Self { - Self { - height, - parent_state_root, - tx_index, - } - } - } - - /// Placeholder for the runtime that will supersede the `avm` orchestrator. - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] - pub struct Runtime; -} - -/// `liba` is the application-facing standard library derived from the existing -/// `program` crate. -pub mod liba { - /// Enumeration of syscalls that applications may perform through `liba`. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum Syscall { - /// Persist or retrieve data from the underlying storage. - Storage, - /// Emit events/logs that become part of transaction receipts. - Log, - /// Perform cryptographic helper operations. - Crypto, - /// Invoke another program within the same block execution. - CrossProgramCall, - } - - /// Minimal handle for the standard library surface exposed to applications. - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] - pub struct Liba; -} diff --git a/crates/os/Cargo.toml b/crates/os/Cargo.toml new file mode 100644 index 0000000..2943e45 --- /dev/null +++ b/crates/os/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "os" +version = "0.1.0" +edition = "2024" +readme = "README.md" + +[dependencies] +avm = { path = "../avm" } +vm = { path = "../vm" } +storage = { path = "../storage" } +types = { path = "../types" } +goblin = "0.10" diff --git a/crates/aos/README.md b/crates/os/README.md similarity index 90% rename from crates/aos/README.md rename to crates/os/README.md index 64df4ec..c154734 100644 --- a/crates/aos/README.md +++ b/crates/os/README.md @@ -1,6 +1,6 @@ -# aOS (Alon's OS) +# OS (Alon's OS) -Alon's OS (aOS) is a minimal operating system purpose-built for deterministic, blockchain-style program execution. It replaces the current `avm` crate with a layered OS: a bootloader for trust establishment, a kernel that orchestrates stateful execution, and `liba`, the standard library that application programs link against. +Alon's OS (OS) is a minimal operating system purpose-built for deterministic, blockchain-style program execution. It replaces the current `avm` crate with a layered OS: a bootloader for trust establishment, a kernel that orchestrates stateful execution, and `liba`, the standard library that application programs link against. ## Goals - Deterministic, replayable execution for consensus environments @@ -46,7 +46,7 @@ Alon's OS (aOS) is a minimal operating system purpose-built for deterministic, b 5. State and receipts are persisted; the resulting state root/receipts are exposed to consensus. ## Relationship to Existing Workspace -- `aos` replaces the `avm` crate as the orchestrator/runtime. +- `os` replaces the `avm` crate as the orchestrator/runtime. - `program` is internalized as `liba` inside this crate (module structure will mirror the current APIs). - `vm`, `state`, `storage`, `types`, and `compiler` remain the core building blocks for CPU execution, state transitions, persistence, shared types, and toolchain support. diff --git a/crates/os/src/bin/bootloader_runner.rs b/crates/os/src/bin/bootloader_runner.rs new file mode 100644 index 0000000..f5b290e --- /dev/null +++ b/crates/os/src/bin/bootloader_runner.rs @@ -0,0 +1,26 @@ +use std::env; +use std::fs; + +use avm::transaction::{Transaction, TransactionBundle, TransactionType}; +use os::bootloader::Bootloader; +use types::address::Address; + +fn main() { + let kernel_path = env::args() + .nth(1) + .expect("pass a path to the kernel ELF as the first argument"); + let kernel_bytes = fs::read(&kernel_path).expect("failed to read kernel ELF"); + + // Temporary: build a single transfer transaction for the bundle. + let bundle = TransactionBundle::new(vec![Transaction { + tx_type: TransactionType::Transfer, + to: Address([0u8; 20]), + from: Address([1u8; 20]), + data: Vec::new(), + value: 0, + nonce: 0, + }]); + + let mut bootloader = Bootloader::new(4, 4096); + bootloader.execute_bundle(&kernel_bytes, &bundle); +} diff --git a/crates/os/src/bootloader.rs b/crates/os/src/bootloader.rs new file mode 100644 index 0000000..c8892e6 --- /dev/null +++ b/crates/os/src/bootloader.rs @@ -0,0 +1,153 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use avm::transaction::{TransactionBundle, TransactionType}; +use goblin::elf::program_header::PT_LOAD; +use goblin::elf::Elf; + +use crate::memory::StackedMemory; +use crate::traps::{TrapAction, TrapFrame, TrapHandler, TrapTable}; +use storage::Storage; +use vm::host_interface::NoopHost; +use vm::memory::{Memory, HEAP_PTR_OFFSET}; +use vm::metering::{MemoryAccessKind, NoopMeter}; +use vm::registers::Register; +use vm::vm::VM; + +/// Boot configuration options consumed by the loader. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BootConfig { + pub debug_console: bool, +} + +impl Default for BootConfig { + fn default() -> Self { + Self { debug_console: true } + } +} + +/// Bootloader skeleton that loads a kernel image into fresh memory and +/// mediates syscall traps until the kernel installs its own handlers. +#[derive(Debug)] +pub struct Bootloader { + pub config: BootConfig, + memory: StackedMemory, + traps: TrapTable, +} + +impl Bootloader { + pub fn new(max_pages: usize, page_size: usize) -> Self { + Self { + config: BootConfig::default(), + memory: StackedMemory::new(max_pages, page_size), + traps: TrapTable::new(), + } + } + + /// Register a syscall trap handler that will run before transferring control to the kernel. + pub fn register_syscall_trap(&mut self, syscall_id: u32, handler: TrapHandler) { + self.traps.register(syscall_id, handler); + } + + /// Load an ELF kernel image into a fresh page and return its entry point + backing memory. + pub fn load_kernel(&mut self, elf_bytes: &[u8]) -> (u32, Memory) { + let elf = Elf::parse(elf_bytes).expect("failed to parse kernel ELF"); + let entry_point = elf.entry as u32; + let page = self.memory.new_page(); + let mut meter = NoopMeter::default(); + let mut highest_byte = 0usize; + + for header in elf + .program_headers + .iter() + .filter(|ph| ph.p_type == PT_LOAD && ph.p_filesz > 0) + { + let offset = header.p_offset as usize; + let file_size = header.p_filesz as usize; + let start = header.p_vaddr as usize; + let end = start + .checked_add(file_size) + .expect("segment size overflow"); + + assert!( + end <= page.size(), + "ELF segment does not fit in a single page (need {}, have {})", + end, + page.size() + ); + + let segment = &elf_bytes[offset..offset + file_size]; + for (idx, byte) in segment.iter().enumerate() { + if !page.store_u8( + start + idx, + *byte, + &mut meter, + MemoryAccessKind::Store, + ) { + panic!("failed to write kernel segment to memory"); + } + } + + highest_byte = highest_byte.max(end); + } + + // Align heap pointer after loaded image. + let heap_start = highest_byte + HEAP_PTR_OFFSET as usize; + page.set_next_heap(heap_start as u32); + + (entry_point, page) + } + + /// Dispatch a syscall trap using the boot-time trap table. + pub fn handle_trap(&self, frame: &mut TrapFrame) -> TrapAction { + self.traps.dispatch(frame) + } + + /// Execute a transaction bundle by delegating to the kernel. This mirrors the + /// AVM entry point where the kernel is responsible for invoking programs. + pub fn execute_bundle(&mut self, kernel_elf: &[u8], bundle: &TransactionBundle) { + let (entry_point, memory) = self.load_kernel(kernel_elf); + let storage = Rc::new(RefCell::new(Storage::new())); + let host: Box = Box::new(NoopHost); + + let mut vm = VM::new(memory.clone(), storage, host); + self.place_bundle(&mut vm, bundle); + vm.cpu.pc = entry_point; + + // TODO: write the bundle into memory for the kernel to consume, and + // extend VM host to forward syscalls into the trap table. + vm.raw_run(); + } + + fn place_bundle(&mut self, vm: &mut VM, bundle: &TransactionBundle) { + let encoded = encode_bundle(bundle); + let addr = vm.set_reg_to_data(Register::A0, &encoded); + // Register a length hint so the kernel can bounds-check the payload. + vm.set_reg_u32(Register::A1, encoded.len() as u32); + // Keep heap aligned after our write. + vm.memory + .set_next_heap((addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32); + } +} + +fn encode_bundle(bundle: &TransactionBundle) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(bundle.transactions.len() as u32).to_le_bytes()); + + for tx in &bundle.transactions { + let tx_type = match tx.tx_type { + TransactionType::Transfer => 0, + TransactionType::CreateAccount => 1, + TransactionType::ProgramCall => 2, + }; + out.push(tx_type); + out.extend_from_slice(&tx.to.0); + out.extend_from_slice(&tx.from.0); + out.extend_from_slice(&(tx.data.len() as u32).to_le_bytes()); + out.extend_from_slice(&tx.data); + out.extend_from_slice(&tx.value.to_le_bytes()); + out.extend_from_slice(&tx.nonce.to_le_bytes()); + } + + out +} diff --git a/crates/os/src/kernel.rs b/crates/os/src/kernel.rs new file mode 100644 index 0000000..e58ba32 --- /dev/null +++ b/crates/os/src/kernel.rs @@ -0,0 +1,78 @@ +use core::slice; +use std::convert::TryInto; + +use avm::transaction::{Transaction, TransactionBundle, TransactionType}; +use types::address::Address; + +/// Kernel entrypoint. Receives a pointer/length pair to an encoded `TransactionBundle` +/// (produced by the bootloader) and walks each transaction. +#[unsafe(no_mangle)] +pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) -> ! { + let encoded = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; + + if let Some(bundle) = decode_bundle(encoded) { + for tx in &bundle.transactions { + execute_transaction(tx); + } + } + + // In a real kernel this would never return; loop forever for now. + loop {} +} + +fn execute_transaction(_tx: &Transaction) { + // TODO: dispatch to programs; for now this is a stub. +} + +fn decode_bundle(encoded: &[u8]) -> Option { + let mut cursor = 0usize; + + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let tx_count_bytes = read(4)?; + let tx_count = u32::from_le_bytes(tx_count_bytes.try_into().ok()?) as usize; + let mut transactions = Vec::with_capacity(tx_count); + + for _ in 0..tx_count { + let tx_type_byte = *read(1)?.first()?; + let tx_type = match tx_type_byte { + 0 => TransactionType::Transfer, + 1 => TransactionType::CreateAccount, + 2 => TransactionType::ProgramCall, + _ => return None, + }; + + let mut to = [0u8; 20]; + to.copy_from_slice(read(20)?); + let mut from = [0u8; 20]; + from.copy_from_slice(read(20)?); + + let data_len_bytes = read(4)?; + let data_len = u32::from_le_bytes(data_len_bytes.try_into().ok()?) as usize; + let data = read(data_len)?.to_vec(); + + let value_bytes = read(8)?; + let value = u64::from_le_bytes(value_bytes.try_into().ok()?); + + let nonce_bytes = read(8)?; + let nonce = u64::from_le_bytes(nonce_bytes.try_into().ok()?); + + transactions.push(Transaction { + tx_type, + to: Address(to), + from: Address(from), + data, + value, + nonce, + }); + } + + Some(TransactionBundle { transactions }) +} diff --git a/crates/os/src/lib.rs b/crates/os/src/lib.rs new file mode 100644 index 0000000..af8a6cc --- /dev/null +++ b/crates/os/src/lib.rs @@ -0,0 +1,14 @@ +//! Deterministic OS scaffold for blockchain-style execution. +//! +//! This crate provides a bootloader skeleton that: +//! - loads a kernel program into fresh pages, +//! - wires a simple syscall trap table, and +//! - hands the loaded image off to a future kernel runtime. +//! +//! Memory utilities are local copies of the VM page primitives to keep the OS +//! independent from the execution engine. + +pub mod bootloader; +pub mod kernel; +pub mod memory; +pub mod traps; diff --git a/crates/os/src/memory/memory_page.rs b/crates/os/src/memory/memory_page.rs new file mode 100644 index 0000000..5fafd1f --- /dev/null +++ b/crates/os/src/memory/memory_page.rs @@ -0,0 +1,330 @@ +use std::cell::{Cell, Ref, RefCell}; +use std::convert::TryInto; +use std::rc::Rc; +use vm::memory::{memory, HEAP_PTR_OFFSET}; +use vm::metering::{MeterResult, Metering, MemoryAccessKind}; + +#[derive(Debug, Clone)] +pub struct MemoryPage { + mem: Rc>>, + pub next_heap: Cell, + pub base_address: usize, // base address for guest memory mapping +} + +impl MemoryPage { + pub fn new_with_base(memory_size: usize, base_address: usize) -> Self { + Self { + mem: Rc::new(RefCell::new(vec![0u8; memory_size])), + next_heap: Cell::new(0), + base_address, + } + } + + pub fn new(memory_size: usize) -> Self { + Self::new_with_base(memory_size, 0) + } + + pub fn mem(&self) -> Ref> { + self.mem.borrow() + } + + pub fn size(&self) -> usize { + let mem = self.mem(); + mem.len() + } + + pub fn offset(&self, addr: usize) -> usize { + addr.checked_sub(self.base_address) + .expect("Address below base_address") + } + + fn meter_access( + metering: &mut dyn Metering, + kind: MemoryAccessKind, + addr: usize, + bytes: usize, + ) -> bool { + matches!( + metering.on_memory_access(kind, addr, bytes), + MeterResult::Continue + ) + } + + pub fn store_u16( + &self, + addr: usize, + val: u16, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 2) { + return false; + } + let offset = self.offset(addr); + let mut mem = self.mem.borrow_mut(); + if offset + 2 > mem.len() { + panic!("store u16 out of bounds: addr = 0x{:08x}", addr); + } + mem[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); + true + } + + pub fn store_u32( + &self, + addr: usize, + val: u32, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 4) { + return false; + } + let offset = self.offset(addr); + let mut mem = self.mem.borrow_mut(); + if offset + 4 > mem.len() { + panic!("store u32 out of bounds: addr = 0x{:08x}", addr); + } + mem[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); + true + } + + pub fn store_u8( + &self, + addr: usize, + val: u8, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 1) { + return false; + } + let offset = self.offset(addr); + let mut mem = self.mem.borrow_mut(); + if offset >= mem.len() { + panic!("store u8 out of bounds: addr = 0x{:08x}", addr); + } + mem[offset] = val; + true + } + + pub fn load_u32( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } + let offset = self.offset(addr); + let mem = self.mem.borrow(); + if offset + 4 > mem.len() { + panic!("load u32 out of bounds: addr = 0x{:08x}", addr); + } + Some(u32::from_le_bytes( + mem[offset..offset + 4].try_into().unwrap(), + )) + } + + pub fn load_byte( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 1) { + return None; + } + let offset = self.offset(addr); + let mem = self.mem.borrow(); + Some(mem[offset]) + } + + pub fn load_halfword( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 2) { + return None; + } + let offset = self.offset(addr); + let mem = self.mem.borrow(); + Some(u16::from_le_bytes( + mem[offset..offset + 2].try_into().unwrap(), + )) + } + + pub fn load_word( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } + let offset = self.offset(addr); + let mem = self.mem.borrow(); + Some(u32::from_le_bytes( + mem[offset..offset + 4].try_into().unwrap(), + )) + } + + pub fn mem_slice(&self, start: usize, end: usize) -> Option> { + let start_offset = self.offset(start); + let end_offset = self.offset(end); + let mem_ref = self.mem.borrow(); + if end_offset > mem_ref.len() || start_offset > end_offset { + return None; + } + Some(std::cell::Ref::map(mem_ref, move |v| &v[start_offset..end_offset])) + } + + pub fn write_code(&self, start_addr: usize, code: &[u8]) { + let start_offset = self.offset(start_addr); + let mut mem = self.mem.borrow_mut(); + let end = start_offset + code.len(); + mem[start_offset..end].copy_from_slice(code); + + // set heap pointer + self.next_heap + .set(start_offset as u32 + code.len() as u32 + HEAP_PTR_OFFSET); + } + + pub fn alloc_on_heap(&self, data: &[u8]) -> u32 { + let mut addr = self.next_heap.get(); + + // Align to 4 bytes (or 8 if you're storing u64s) + let align = 8; + addr = (addr + (align - 1)) & !(align - 1); + + let end = addr + data.len() as u32; + assert!( + end as usize <= self.size(), + "Out of memory: trying to allocate {} bytes, but only {} bytes available", + data.len(), + self.size() - addr as usize + ); + + self.mem.borrow_mut()[addr as usize..end as usize].copy_from_slice(data); + self.next_heap.set(end); + + addr + } + + pub fn stack_top(&self) -> u32 { + self.size() as u32 + } +} + +impl Default for MemoryPage { + fn default() -> Self { + MemoryPage::new(4096) + } +} + +impl memory for MemoryPage { + fn mem(&self) -> Ref> { + MemoryPage::mem(self) + } + + fn mem_slice(&self, start: usize, end: usize) -> Option> { + MemoryPage::mem_slice(self, start, end) + } + + fn store_u16( + &self, + addr: usize, + val: u16, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + MemoryPage::store_u16(self, addr, val, metering, kind) + } + + fn store_u32( + &self, + addr: usize, + val: u32, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + MemoryPage::store_u32(self, addr, val, metering, kind) + } + + fn store_u8( + &self, + addr: usize, + val: u8, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + MemoryPage::store_u8(self, addr, val, metering, kind) + } + + fn load_u32( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_u32(self, addr, metering, kind) + } + + fn load_byte( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_byte(self, addr, metering, kind) + } + + fn load_halfword( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_halfword(self, addr, metering, kind) + } + + fn load_word( + &self, + addr: usize, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + MemoryPage::load_word(self, addr, metering, kind) + } + + fn write_code(&self, start_addr: usize, code: &[u8]) { + MemoryPage::write_code(self, start_addr, code) + } + + fn alloc_on_heap(&self, data: &[u8]) -> u32 { + MemoryPage::alloc_on_heap(self, data) + } + + fn stack_top(&self) -> u32 { + MemoryPage::stack_top(self) + } + + fn size(&self) -> usize { + MemoryPage::size(self) + } + + fn offset(&self, addr: usize) -> usize { + MemoryPage::offset(self, addr) + } + + fn next_heap(&self) -> u32 { + self.next_heap.get() + } + + fn set_next_heap(&self, next: u32) { + self.next_heap.set(next); + } +} diff --git a/crates/os/src/memory/mod.rs b/crates/os/src/memory/mod.rs new file mode 100644 index 0000000..043d75e --- /dev/null +++ b/crates/os/src/memory/mod.rs @@ -0,0 +1,7 @@ +//! Simple in-memory pages for the OS boot/runtime layers. + +mod memory_page; +mod stacked_memory; + +pub use memory_page::MemoryPage; +pub use stacked_memory::StackedMemory; diff --git a/crates/os/src/memory/stacked_memory.rs b/crates/os/src/memory/stacked_memory.rs new file mode 100644 index 0000000..a5106c7 --- /dev/null +++ b/crates/os/src/memory/stacked_memory.rs @@ -0,0 +1,55 @@ +use std::rc::Rc; + +use super::MemoryPage; +use vm::memory::Memory; + +/// Manages a stack of memory pages allocated in order. +#[derive(Debug)] +pub struct StackedMemory { + pub page_size: usize, + max_pages: usize, + pages: Vec, +} + +impl StackedMemory { + pub fn new(max_pages: usize, page_size: usize) -> Self { + assert!(max_pages != 0, "max_pages must be > 0"); + assert!(page_size != 0, "page_size must be > 0"); + + Self { + page_size, + max_pages, + pages: Vec::with_capacity(max_pages), + } + } + + /// Creates and owns a new page. + pub fn new_page(&mut self) -> Memory { + if self.pages.len() >= self.max_pages { + panic!( + "out of memory: maximum page count ({}) reached", + self.max_pages + ); + } + + let page: Memory = Rc::new(MemoryPage::new(self.page_size)); + self.pages.push(Rc::clone(&page)); + page + } + + pub fn pop_page(&mut self) { + self.pages.pop(); + } + + pub fn get_page(&self, index: usize) -> Option { + self.pages.get(index).cloned() + } + + pub fn top_page(&self) -> Option { + self.pages.last().cloned() + } + + pub fn count(&self) -> usize { + self.pages.len() + } +} diff --git a/crates/os/src/traps.rs b/crates/os/src/traps.rs new file mode 100644 index 0000000..41af317 --- /dev/null +++ b/crates/os/src/traps.rs @@ -0,0 +1,67 @@ +use std::collections::HashMap; +use std::fmt; + +/// Outcome after a trap handler runs. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TrapAction { + Continue, + KillKernel, +} + +/// Lightweight snapshot of CPU state for a syscall trap. +#[derive(Debug, Clone)] +pub struct TrapFrame { + pub syscall_id: u32, + pub args: [u32; 6], + pub pc: usize, +} + +pub type TrapHandler = Box TrapAction + Send + Sync>; + +/// Syscall IDs mirrored from the VM syscall table. +pub mod syscall { + pub const STORAGE_GET: u32 = 1; + pub const STORAGE_SET: u32 = 2; + pub const PANIC: u32 = 3; + pub const LOG: u32 = 4; + pub const CALL_PROGRAM: u32 = 5; + pub const FIRE_EVENT: u32 = 6; + pub const ALLOC: u32 = 7; + pub const DEALLOC: u32 = 8; + pub const TRANSFER: u32 = 9; + pub const BALANCE: u32 = 10; +} + +/// Simple table that dispatches traps by syscall id. +#[derive(Default)] +pub struct TrapTable { + handlers: HashMap, +} + +impl fmt::Debug for TrapTable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrapTable") + .field("registered", &self.handlers.len()) + .finish() + } +} + +impl TrapTable { + pub fn new() -> Self { + Self { + handlers: HashMap::new(), + } + } + + pub fn register(&mut self, syscall_id: u32, handler: TrapHandler) { + self.handlers.insert(syscall_id, handler); + } + + pub fn dispatch(&self, frame: &mut TrapFrame) -> TrapAction { + if let Some(handler) = self.handlers.get(&frame.syscall_id) { + handler(frame) + } else { + TrapAction::KillKernel + } + } +} From 8458ee18b1c629f4bad659e174f4005a0e670bbb Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 16 Dec 2025 20:58:34 +0100 Subject: [PATCH 07/70] kernel skeleton --- .gitignore | 3 + Cargo.lock | 4 +- Makefile | 9 +- crates/avm/src/transaction.rs | 46 +------ crates/examples/Cargo.toml | 1 + crates/examples/tests/common/test_runner.rs | 124 +++++++------------ crates/os/Cargo.toml | 17 ++- crates/os/src/allocator.rs | 41 +++++++ crates/os/src/bin/bootloader_runner.rs | 2 +- crates/os/src/bootloader.rs | 104 ++++++---------- crates/os/src/kernel.rs | 128 +++++++++++--------- crates/os/src/lib.rs | 7 +- crates/os/src/traps.rs | 30 ++++- crates/program/Cargo.toml | 6 +- crates/program/src/lib.rs | 9 +- crates/program/src/panic.rs | 38 +++--- crates/program/src/parser.rs | 1 - crates/types/src/lib.rs | 7 +- crates/types/src/transaction.rs | 126 +++++++++++++++++++ crates/vm/src/vm.rs | 1 - 20 files changed, 419 insertions(+), 285 deletions(-) create mode 100644 crates/os/src/allocator.rs create mode 100644 crates/types/src/transaction.rs diff --git a/.gitignore b/.gitignore index 2da6768..c24e220 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,9 @@ no_llvm_build # Created by nix dev shell / .envrc src/tools/nix-dev-shell/flake.lock +# Project outputs +crates/os/bin/ + ## ICE reports rustc-ice-*.txt diff --git a/Cargo.lock b/Cargo.lock index 1dbcc29..1a41d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,7 @@ dependencies = [ "compiler", "k256", "once_cell", + "os", "program", "serde_json", "sha2", @@ -259,8 +260,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" name = "os" version = "0.1.0" dependencies = [ - "avm", + "compiler", "goblin", + "program", "storage", "types", "vm", diff --git a/Makefile b/Makefile index 71edb5c..65435ca 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,9 @@ .PHONY: all +# Nightly cargo for avm32 builds (used for kernel ELF and examples). +CARGO_NIGHTLY ?= cargo +nightly-aarch64-apple-darwin +AVM32 := $(CARGO_NIGHTLY) run -p compiler --bin avm32 -- + all: clean program examples test utils summary .PHONY: run_examples @@ -7,8 +11,11 @@ all: clean program examples test utils summary run_examples: @echo "=== Building example programs ===" RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples + @echo "=== Building kernel ELF ===" + @mkdir -p crates/os/bin + @$(AVM32) all --bin kernel --manifest-path crates/os/Cargo.toml --features guest_kernel --out-dir crates/os/bin @echo "=== Running example crate tests ===" - cd crates/examples && RUSTFLAGS="-Awarnings" cargo test -- --nocapture + cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../os/bin/kernel.elf" cargo test test_examples -- --nocapture @echo "=== Example programs build and tests complete ===" clean: diff --git a/crates/avm/src/transaction.rs b/crates/avm/src/transaction.rs index 182c428..9a733c8 100644 --- a/crates/avm/src/transaction.rs +++ b/crates/avm/src/transaction.rs @@ -1,47 +1,3 @@ use types::address::Address; -#[derive(Debug, Clone)] -pub enum TransactionType { - /// Type 0 - Regular value transfer (not a contract) - Transfer = 0, - - /// Type 1 - Account create with program data (contract deployment) - CreateAccount = 1, - - /// Type 2 - Contract call (calling into existing code) - ProgramCall = 2, -} - -#[derive(Debug, Clone)] -pub struct Transaction { - pub tx_type: TransactionType, // type of transaction - pub to: Address, // recipient address - pub from: Address, // sender public key/address - pub data: Vec, // input data - pub value: u64, // amount/value sent - pub nonce: u64, // transaction nonce -} - -/// Holds a set of transactions to be processed as a unit -#[derive(Debug, Clone)] -pub struct TransactionBundle { - pub transactions: Vec, -} - -impl TransactionBundle { - pub fn new(transactions: Vec) -> Self { - TransactionBundle { transactions } - } - - pub fn add_transaction(&mut self, tx: Transaction) { - self.transactions.push(tx); - } - - pub fn len(&self) -> usize { - self.transactions.len() - } - - pub fn is_empty(&self) -> bool { - self.transactions.is_empty() - } -} +pub use types::transaction::{Transaction, TransactionBundle, TransactionType}; diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 707c6e8..e430fcb 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -13,6 +13,7 @@ types = { path = "../types" } # adjust path as needed compiler = { path = "../compiler" } # adjust path as needed avm = { path = "../avm" } # adjust path as needed state = { path = "../state" } +os = { path = "../os" } once_cell = "1.19.0" serde_json = "1.0" diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index aae713c..af1b5f0 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -1,15 +1,14 @@ #![allow(dead_code)] -use avm::avm::AVM; -use state::State; -use super::utils::to_address; -use super::state_helper::test_state; +use std::env; +use std::fs::{self, File}; +use std::io::Write as IoWrite; +use std::path::Path; use std::rc::Rc; + use core::cell::RefCell; use core::fmt::Write; -use std::fs::File; -use std::io::Write as IoWrite; -use std::path::Path; +use os::bootloader::Bootloader; // File writer for logging to disk struct FileWriter { @@ -48,6 +47,8 @@ pub struct TestRunner { verbose: bool, vm_memory_size: usize, max_memory_pages: usize, + kernel_bytes: Option>, + kernel_path: Option, } impl TestRunner { @@ -87,6 +88,8 @@ impl TestRunner { verbose: false, vm_memory_size: 512 * 1024, // larger default to accommodate bigger binaries without RVC max_memory_pages: 128, // allow more pages for larger programs + kernel_bytes: Self::load_kernel_from_env(), + kernel_path: env::var("KERNEL_ELF").ok(), } } @@ -110,13 +113,7 @@ impl TestRunner { /// Run a single test case fn run_test_case(&self, case: &super::TestCase) -> Result<(), String> { - let transactions = case.bundle.transactions.clone(); - let test_state = super::state_helper::test_state(); - let mut avm = AVM::new(self.max_memory_pages, self.vm_memory_size, test_state); - - // Set up AVM with the chosen writer and verbosity - avm.set_verbosity(self.verbose); - avm.set_verbose_writer(self.writer.clone()); + let mut bootloader = Bootloader::new(self.max_memory_pages, self.vm_memory_size); // Write test case header writeln!(self.writer.borrow_mut(), "\n############################################").unwrap(); @@ -132,75 +129,34 @@ impl TestRunner { } writeln!(self.writer.borrow_mut()).unwrap(); - let mut last_success: bool = false; - let mut last_error_code: u32 = 0; - let mut last_result: Option = None; - - for tx in transactions { - // Log the transaction details - writeln!(self.writer.borrow_mut(), - "Running {:?} tx:\n From: {:?}\n To: {:?}\n Data len: {:?}", - tx.tx_type, tx.from, tx.to, tx.data.len() - ).unwrap(); - - let receipt = avm.run_tx(tx); - last_success = receipt.result.success; - last_error_code = receipt.result.error_code; - last_result = Some(receipt.result.clone()); - - writeln!(self.writer.borrow_mut(), "⛽ Gas used so far: {}", avm.gas_used()).unwrap(); - - // Write state dump - writeln!(self.writer.borrow_mut(), "--- State Dump ---").unwrap(); - for (address, account) in &avm.state.accounts { - writeln!(self.writer.borrow_mut(), " 🔑 Address: 0x{}", address).unwrap(); - writeln!(self.writer.borrow_mut(), " - Balance: {}", account.balance).unwrap(); - writeln!(self.writer.borrow_mut(), " - Nonce: {}", account.nonce).unwrap(); - writeln!(self.writer.borrow_mut(), " - Is contract?: {}", account.is_contract).unwrap(); - writeln!(self.writer.borrow_mut(), " - Code size: {} bytes", account.code.len()).unwrap(); - writeln!(self.writer.borrow_mut(), " - Storage:").unwrap(); - for (key, value) in &account.storage { - writeln!(self.writer.borrow_mut(), " [{:?}] = {:?}", key, value).unwrap(); - } - writeln!(self.writer.borrow_mut(), "").unwrap(); - } - writeln!(self.writer.borrow_mut(), "--------------------").unwrap(); - - // Write receipt - if let Some(abi) = &case.abi { - let mut writer = self.writer.borrow_mut(); - writeln!(writer, "=== Transaction Receipt ===").unwrap(); - writeln!(writer, "From: {:?}", receipt.tx.from).unwrap(); - writeln!(writer, "To: {:?}", receipt.tx.to).unwrap(); - writeln!(writer, "Result: {:?}", receipt.result).unwrap(); - writeln!(writer, "Events:").unwrap(); - receipt.print_events_pretty(abi, &mut *writer); - } else { - writeln!(self.writer.borrow_mut(), "{}", receipt).unwrap(); - } - } - - // Perform assertions - if last_success != case.expected_success { - return Err(format!("{}: expected success={}, got={}", - case.name, case.expected_success, last_success)); - } - - if last_error_code != case.expected_error_code { - return Err(format!("{}: expected error_code={}, got={}", - case.name, case.expected_error_code, last_error_code)); + // Load kernel via bootloader before executing transactions with the AVM path. + if let Some(kernel) = &self.kernel_bytes { + writeln!( + self.writer.borrow_mut(), + "🚀 Booting kernel ELF {} via bootloader...", + self.kernel_path + .as_deref() + .unwrap_or("") + ) + .unwrap(); + bootloader.execute_bundle(kernel, &case.bundle); + } else { + writeln!( + self.writer.borrow_mut(), + "⚠️ KERNEL_ELF not set or unreadable; skipping bootloader run." + ) + .unwrap(); } - // Check expected data if specified - if let Some(expected_data) = &case.expected_data { - if let Some(result) = last_result { - let actual_data = &result.data[..result.data_len as usize]; - if actual_data != expected_data.as_slice() { - return Err(format!("{}: expected data mismatch", case.name)); - } - } - } + // Execute the whole bundle via the bootloader/kernel path. + bootloader.execute_bundle( + self.kernel_bytes.as_ref().ok_or_else(|| { + "KERNEL_ELF not set or unreadable; bootloader path required".to_string() + })?, + &case.bundle, + ); + // For now we treat successful bootloader execution as a passed test. Ok(()) } } @@ -210,3 +166,11 @@ impl Default for TestRunner { Self::with_writer(Rc::new(RefCell::new(ConsoleWriter))) } } + +impl TestRunner { + fn load_kernel_from_env() -> Option> { + let path = env::var("KERNEL_ELF") + .unwrap_or_else(|_| "crates/os/bin/kernel.elf".to_string()); + fs::read(&path).ok() + } +} diff --git a/crates/os/Cargo.toml b/crates/os/Cargo.toml index 2943e45..78c951e 100644 --- a/crates/os/Cargo.toml +++ b/crates/os/Cargo.toml @@ -4,9 +4,22 @@ version = "0.1.0" edition = "2024" readme = "README.md" +[features] +guest_kernel = [] + [dependencies] -avm = { path = "../avm" } +types = { path = "../types" } + +[target.'cfg(not(target_arch = "riscv32"))'.dependencies] vm = { path = "../vm" } storage = { path = "../storage" } -types = { path = "../types" } goblin = "0.10" +compiler = { path = "../compiler" } + +[target.'cfg(target_arch = "riscv32")'.dependencies] +program = { path = "../program", default-features = false } + +[[bin]] +name = "kernel" +path = "src/kernel.rs" +required-features = ["guest_kernel"] diff --git a/crates/os/src/allocator.rs b/crates/os/src/allocator.rs new file mode 100644 index 0000000..d832140 --- /dev/null +++ b/crates/os/src/allocator.rs @@ -0,0 +1,41 @@ +//! Minimal allocator used by the guest kernel. For riscv32 it delegates to +//! syscalls 7/8. +#![cfg(target_arch = "riscv32")] + +use core::alloc::{GlobalAlloc, Layout}; +use core::arch::asm; + +#[derive(Debug)] +pub struct VmAllocator; + +unsafe impl GlobalAlloc for VmAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + syscall_alloc(layout.size(), layout.align()) + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + syscall_dealloc(ptr, layout.size()); + } +} + +unsafe fn syscall_alloc(size: usize, align: usize) -> *mut u8 { + let mut result: usize; + asm!( + "li a7, 7", // SYSCALL_ALLOC + "ecall", + in("a1") size, + in("a2") align, + out("a0") result, + ); + result as *mut u8 +} + +unsafe fn syscall_dealloc(ptr: *mut u8, size: usize) { + asm!( + "li a7, 8", // SYSCALL_DEALLOC + "ecall", + in("a1") ptr as usize, + in("a2") size, + options(nostack, preserves_flags), + ); +} diff --git a/crates/os/src/bin/bootloader_runner.rs b/crates/os/src/bin/bootloader_runner.rs index f5b290e..86e897b 100644 --- a/crates/os/src/bin/bootloader_runner.rs +++ b/crates/os/src/bin/bootloader_runner.rs @@ -1,8 +1,8 @@ use std::env; use std::fs; -use avm::transaction::{Transaction, TransactionBundle, TransactionType}; use os::bootloader::Bootloader; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; use types::address::Address; fn main() { diff --git a/crates/os/src/bootloader.rs b/crates/os/src/bootloader.rs index c8892e6..0b4235f 100644 --- a/crates/os/src/bootloader.rs +++ b/crates/os/src/bootloader.rs @@ -1,8 +1,9 @@ use std::cell::RefCell; use std::rc::Rc; +use std::vec::Vec; -use avm::transaction::{TransactionBundle, TransactionType}; -use goblin::elf::program_header::PT_LOAD; +use types::transaction::TransactionBundle; +use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; use crate::memory::StackedMemory; @@ -10,7 +11,6 @@ use crate::traps::{TrapAction, TrapFrame, TrapHandler, TrapTable}; use storage::Storage; use vm::host_interface::NoopHost; use vm::memory::{Memory, HEAP_PTR_OFFSET}; -use vm::metering::{MemoryAccessKind, NoopMeter}; use vm::registers::Register; use vm::vm::VM; @@ -51,50 +51,42 @@ impl Bootloader { /// Load an ELF kernel image into a fresh page and return its entry point + backing memory. pub fn load_kernel(&mut self, elf_bytes: &[u8]) -> (u32, Memory) { - let elf = Elf::parse(elf_bytes).expect("failed to parse kernel ELF"); - let entry_point = elf.entry as u32; + let elf = parse_elf_from_bytes(elf_bytes).expect("failed to parse kernel ELF"); + let entry_point = Elf::parse(elf_bytes) + .expect("failed to parse entry point") + .entry as u32; + + let (code, code_base) = elf + .get_flat_code() + .expect("kernel ELF missing .text"); + let (rodata, ro_base) = elf.get_flat_rodata().unwrap_or((Vec::new(), code_base)); + + let min_base = core::cmp::min(code_base, ro_base) as usize; + let code_end = (code_base + code.len() as u64) as usize; + let ro_end = (ro_base + rodata.len() as u64) as usize; + let image_end = core::cmp::max(code_end, ro_end); + let image_size = image_end + .checked_sub(min_base) + .expect("invalid image size"); + let page = self.memory.new_page(); - let mut meter = NoopMeter::default(); - let mut highest_byte = 0usize; - - for header in elf - .program_headers - .iter() - .filter(|ph| ph.p_type == PT_LOAD && ph.p_filesz > 0) - { - let offset = header.p_offset as usize; - let file_size = header.p_filesz as usize; - let start = header.p_vaddr as usize; - let end = start - .checked_add(file_size) - .expect("segment size overflow"); - - assert!( - end <= page.size(), - "ELF segment does not fit in a single page (need {}, have {})", - end, - page.size() - ); - - let segment = &elf_bytes[offset..offset + file_size]; - for (idx, byte) in segment.iter().enumerate() { - if !page.store_u8( - start + idx, - *byte, - &mut meter, - MemoryAccessKind::Store, - ) { - panic!("failed to write kernel segment to memory"); - } - } - - highest_byte = highest_byte.max(end); + assert!( + image_end <= page.size(), + "ELF image does not fit in a single page (need {}, have {})", + image_end, + page.size() + ); + + // Flatten code + rodata into a single buffer and write once to set heap pointer properly. + let mut image = vec![0u8; image_size]; + let code_off = (code_base as usize).saturating_sub(min_base); + image[code_off..code_off + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + let ro_off = (ro_base as usize).saturating_sub(min_base); + image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); } - // Align heap pointer after loaded image. - let heap_start = highest_byte + HEAP_PTR_OFFSET as usize; - page.set_next_heap(heap_start as u32); - + page.write_code(min_base, &image); (entry_point, page) } @@ -120,7 +112,7 @@ impl Bootloader { } fn place_bundle(&mut self, vm: &mut VM, bundle: &TransactionBundle) { - let encoded = encode_bundle(bundle); + let encoded = bundle.encode(); let addr = vm.set_reg_to_data(Register::A0, &encoded); // Register a length hint so the kernel can bounds-check the payload. vm.set_reg_u32(Register::A1, encoded.len() as u32); @@ -129,25 +121,3 @@ impl Bootloader { .set_next_heap((addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32); } } - -fn encode_bundle(bundle: &TransactionBundle) -> Vec { - let mut out = Vec::new(); - out.extend_from_slice(&(bundle.transactions.len() as u32).to_le_bytes()); - - for tx in &bundle.transactions { - let tx_type = match tx.tx_type { - TransactionType::Transfer => 0, - TransactionType::CreateAccount => 1, - TransactionType::ProgramCall => 2, - }; - out.push(tx_type); - out.extend_from_slice(&tx.to.0); - out.extend_from_slice(&tx.from.0); - out.extend_from_slice(&(tx.data.len() as u32).to_le_bytes()); - out.extend_from_slice(&tx.data); - out.extend_from_slice(&tx.value.to_le_bytes()); - out.extend_from_slice(&tx.nonce.to_le_bytes()); - } - - out -} diff --git a/crates/os/src/kernel.rs b/crates/os/src/kernel.rs index e58ba32..b5835ed 100644 --- a/crates/os/src/kernel.rs +++ b/crates/os/src/kernel.rs @@ -1,78 +1,88 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +#[cfg(target_arch = "riscv32")] +mod allocator; +use program::log; +use program::logf; + use core::slice; -use std::convert::TryInto; +use core::mem::forget; +use types::transaction::{Transaction, TransactionBundle}; -use avm::transaction::{Transaction, TransactionBundle, TransactionType}; -use types::address::Address; +#[cfg(target_arch = "riscv32")] +#[global_allocator] +static ALLOC: allocator::VmAllocator = allocator::VmAllocator; /// Kernel entrypoint. Receives a pointer/length pair to an encoded `TransactionBundle` /// (produced by the bootloader) and walks each transaction. #[unsafe(no_mangle)] -pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) -> ! { - let encoded = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; +pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) { + // Copy args to locals before any syscalls (ecall clobbers a0). + let ptr = bundle_ptr; + let len = bundle_len; + + log!("kernel boot"); + logf!("bundle_len=%d", len as u32); + + let encoded = unsafe { slice::from_raw_parts(ptr, len) }; - if let Some(bundle) = decode_bundle(encoded) { - for tx in &bundle.transactions { - execute_transaction(tx); + if let Some(bundle) = TransactionBundle::decode(encoded) { + let count = bundle.transactions.len(); + logf!("decoded tx count=%d", count as u32); + for i in 0..count { + logf!("processing tx %d/%d", (i + 1) as u32, count as u32); + if let Some(tx) = bundle.transactions.get(i) { + execute_transaction(tx); + } else { + logf!("missing tx at index %d", i as u32); + } } + // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. + forget(bundle); + } else { + log!("bundle decode failed"); } - - // In a real kernel this would never return; loop forever for now. - loop {} + log!("finished bundle execution"); + halt(); } fn execute_transaction(_tx: &Transaction) { - // TODO: dispatch to programs; for now this is a stub. + log!("executing transaction"); } -fn decode_bundle(encoded: &[u8]) -> Option { - let mut cursor = 0usize; - - let mut read = |len: usize| -> Option<&[u8]> { - if cursor + len > encoded.len() { - return None; - } - let slice = &encoded[cursor..cursor + len]; - cursor += len; - Some(slice) - }; - - let tx_count_bytes = read(4)?; - let tx_count = u32::from_le_bytes(tx_count_bytes.try_into().ok()?) as usize; - let mut transactions = Vec::with_capacity(tx_count); +#[inline(never)] +fn halt() -> ! { + // Signal completion to the host by triggering a trap and stop execution. + unsafe { core::arch::asm!("ebreak") }; + loop {} +} - for _ in 0..tx_count { - let tx_type_byte = *read(1)?.first()?; - let tx_type = match tx_type_byte { - 0 => TransactionType::Transfer, - 1 => TransactionType::CreateAccount, - 2 => TransactionType::ProgramCall, - _ => return None, +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + #[cfg(target_arch = "riscv32")] + { + let msg_bytes = if let Some(s) = info.message().as_str() { + s.as_bytes() + } else { + b"kernel panic (non-str message)" }; - - let mut to = [0u8; 20]; - to.copy_from_slice(read(20)?); - let mut from = [0u8; 20]; - from.copy_from_slice(read(20)?); - - let data_len_bytes = read(4)?; - let data_len = u32::from_le_bytes(data_len_bytes.try_into().ok()?) as usize; - let data = read(data_len)?.to_vec(); - - let value_bytes = read(8)?; - let value = u64::from_le_bytes(value_bytes.try_into().ok()?); - - let nonce_bytes = read(8)?; - let nonce = u64::from_le_bytes(nonce_bytes.try_into().ok()?); - - transactions.push(Transaction { - tx_type, - to: Address(to), - from: Address(from), - data, - value, - nonce, - }); + unsafe { + core::arch::asm!( + "li a7, 3", // SYSCALL_PANIC + "ecall", + in("a0") msg_bytes.as_ptr(), + in("a1") msg_bytes.len(), + ); + core::arch::asm!("ebreak", options(nomem, nostack)); + } + loop {} } - Some(TransactionBundle { transactions }) + #[cfg(not(target_arch = "riscv32"))] + { + panic!("kernel panic: {:?}", info); + } } diff --git a/crates/os/src/lib.rs b/crates/os/src/lib.rs index af8a6cc..0a03472 100644 --- a/crates/os/src/lib.rs +++ b/crates/os/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(target_arch = "riscv32", no_std)] //! Deterministic OS scaffold for blockchain-style execution. //! //! This crate provides a bootloader skeleton that: @@ -8,7 +9,11 @@ //! Memory utilities are local copies of the VM page primitives to keep the OS //! independent from the execution engine. +#[cfg(target_arch = "riscv32")] +pub mod allocator; +#[cfg(not(target_arch = "riscv32"))] pub mod bootloader; -pub mod kernel; +#[cfg(not(target_arch = "riscv32"))] pub mod memory; +#[cfg(not(target_arch = "riscv32"))] pub mod traps; diff --git a/crates/os/src/traps.rs b/crates/os/src/traps.rs index 41af317..e76da53 100644 --- a/crates/os/src/traps.rs +++ b/crates/os/src/traps.rs @@ -48,9 +48,11 @@ impl fmt::Debug for TrapTable { impl TrapTable { pub fn new() -> Self { - Self { + let mut table = Self { handlers: HashMap::new(), - } + }; + table.register_logging_stubs(); + table } pub fn register(&mut self, syscall_id: u32, handler: TrapHandler) { @@ -64,4 +66,28 @@ impl TrapTable { TrapAction::KillKernel } } + + /// Install a default stub for every VM syscall that just logs the call. + pub fn register_logging_stubs(&mut self) { + self.register(syscall::STORAGE_GET, log_stub("STORAGE_GET")); + self.register(syscall::STORAGE_SET, log_stub("STORAGE_SET")); + self.register(syscall::PANIC, log_stub("PANIC")); + self.register(syscall::LOG, log_stub("LOG")); + self.register(syscall::CALL_PROGRAM, log_stub("CALL_PROGRAM")); + self.register(syscall::FIRE_EVENT, log_stub("FIRE_EVENT")); + self.register(syscall::ALLOC, log_stub("ALLOC")); + self.register(syscall::DEALLOC, log_stub("DEALLOC")); + self.register(syscall::TRANSFER, log_stub("TRANSFER")); + self.register(syscall::BALANCE, log_stub("BALANCE")); + } +} + +fn log_stub(name: &'static str) -> TrapHandler { + Box::new(move |frame: &mut TrapFrame| { + println!( + "trap {} called (id={}, pc=0x{:08x}, args={:?})", + name, frame.syscall_id, frame.pc, frame.args + ); + TrapAction::Continue + }) } diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml index 3612d98..f39abbb 100644 --- a/crates/program/Cargo.toml +++ b/crates/program/Cargo.toml @@ -3,5 +3,9 @@ name = "program" version = "0.1.0" edition = "2024" +[features] +default = ["guest_handlers"] +guest_handlers = [] + [dependencies] -types = { path = "../types" } +types = { path = "../types" } diff --git a/crates/program/src/lib.rs b/crates/program/src/lib.rs index 42b3558..875e1d1 100644 --- a/crates/program/src/lib.rs +++ b/crates/program/src/lib.rs @@ -54,20 +54,17 @@ pub use storage::PERSISTENT_DOMAIN; // Allow `$crate::PERSISTENT_DOMAIN` in macr pub mod router; pub use router::{decode_calls, route, FuncCall}; -// Panic handling -mod panic; +// Panic helper (and panic handler when guest_handlers enabled) +pub mod panic; pub use panic::vm_panic; // Memory allocator pub mod allocator; -// Global allocator - automatically provides heap allocation for all guest programs -// Only enable for RISC-V target to avoid recursion on host -#[cfg(target_arch = "riscv32")] +#[cfg(all(target_arch = "riscv32", feature = "guest_handlers"))] #[global_allocator] static ALLOCATOR: allocator::VmAllocator = allocator::VmAllocator; - /* --------------------------- Assertion Utilities -------------------------- */ /// Aborts execution if condition is false, printing `msg`. diff --git a/crates/program/src/panic.rs b/crates/program/src/panic.rs index a64ca43..9042cb6 100644 --- a/crates/program/src/panic.rs +++ b/crates/program/src/panic.rs @@ -1,30 +1,36 @@ -use crate::logf; +//! Panic helper and handler for guest programs. -#[cfg(target_arch = "riscv32")] +/// Trap into the host with a panic message. +#[inline(always)] pub fn vm_panic(msg: &[u8]) -> ! { + #[cfg(target_arch = "riscv32")] unsafe { core::arch::asm!( - "li a7, 3", // syscall: panic + "li a7, 3", // SYSCALL_PANIC "ecall", in("a0") msg.as_ptr(), in("a1") msg.len(), + options(noreturn), ); - core::arch::asm!("ebreak", options(nomem, nostack)); } - loop {} -} -#[cfg(target_arch = "riscv32")] -#[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { - if let Some(s) = info.message().as_str() { - vm_panic(s.as_bytes()); - } else { - vm_panic(b"panic occurred (non-str message)"); + #[cfg(not(target_arch = "riscv32"))] + { + panic!( + "vm_panic: {}", + core::str::from_utf8(msg).unwrap_or("") + ); } } -#[cfg(not(target_arch = "riscv32"))] -pub fn vm_panic(msg: &[u8]) -> ! { - panic!("vm_panic: {}", core::str::from_utf8(msg).unwrap_or("")); +/// Guest panic handler for RISC-V builds (only when guest_handlers enabled). +#[cfg(all(target_arch = "riscv32", feature = "guest_handlers"))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + let msg_bytes = if let Some(s) = info.message().as_str() { + s.as_bytes() + } else { + b"guest panic" + }; + vm_panic(msg_bytes); } diff --git a/crates/program/src/parser.rs b/crates/program/src/parser.rs index 38f8cf5..843be18 100644 --- a/crates/program/src/parser.rs +++ b/crates/program/src/parser.rs @@ -1,5 +1,4 @@ //! Simple parser for reading typed values from a byte slice. -use core::convert::TryInto; use types::address::Address; use crate::vm_panic; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 4625de0..d46eb86 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] +extern crate alloc; + pub mod address; pub use address::Address; @@ -13,8 +15,11 @@ pub use o::*; // Allow `$crate::O` in macros pub mod primitives; pub use primitives::*; +pub mod transaction; +pub use transaction::*; + // used for serialization pub trait SerializeField { /// Appends `self` into `buf` at `*offset`, advancing the offset. fn serialize_field(&self, buf: &mut [u8], offset: &mut usize); -} \ No newline at end of file +} diff --git a/crates/types/src/transaction.rs b/crates/types/src/transaction.rs new file mode 100644 index 0000000..f64ea65 --- /dev/null +++ b/crates/types/src/transaction.rs @@ -0,0 +1,126 @@ +use alloc::vec::Vec; +use core::convert::TryInto; + +use crate::address::Address; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransactionType { + /// Type 0 - Regular value transfer (not a contract) + Transfer = 0, + /// Type 1 - Account create with program data (contract deployment) + CreateAccount = 1, + /// Type 2 - Contract call (calling into existing code) + ProgramCall = 2, +} + +impl TransactionType { + pub fn from_u8(value: u8) -> Option { + match value { + 0 => Some(TransactionType::Transfer), + 1 => Some(TransactionType::CreateAccount), + 2 => Some(TransactionType::ProgramCall), + _ => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct Transaction { + pub tx_type: TransactionType, // type of transaction + pub to: Address, // recipient address + pub from: Address, // sender public key/address + pub data: Vec, // input data + pub value: u64, // amount/value sent + pub nonce: u64, // transaction nonce +} + +/// Holds a set of transactions to be processed as a unit. +#[derive(Debug, Clone)] +pub struct TransactionBundle { + pub transactions: Vec, +} + +impl TransactionBundle { + pub fn new(transactions: Vec) -> Self { + TransactionBundle { transactions } + } + + pub fn add_transaction(&mut self, tx: Transaction) { + self.transactions.push(tx); + } + + pub fn len(&self) -> usize { + self.transactions.len() + } + + pub fn is_empty(&self) -> bool { + self.transactions.is_empty() + } + + /// Encode the bundle into a flat little-endian buffer that can be copied into guest memory. + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(self.transactions.len() as u32).to_le_bytes()); + + for tx in &self.transactions { + out.push(tx.tx_type as u8); + out.extend_from_slice(&tx.to.0); + out.extend_from_slice(&tx.from.0); + out.extend_from_slice(&(tx.data.len() as u32).to_le_bytes()); + out.extend_from_slice(&tx.data); + out.extend_from_slice(&tx.value.to_le_bytes()); + out.extend_from_slice(&tx.nonce.to_le_bytes()); + } + + out + } + + /// Decode a buffer produced by `encode` back into a bundle. + pub fn decode(encoded: &[u8]) -> Option { + let mut cursor = 0usize; + + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let tx_count_bytes = read(4)?; + let tx_count = u32::from_le_bytes(tx_count_bytes.try_into().ok()?) as usize; + let mut transactions = Vec::with_capacity(tx_count); + + for _ in 0..tx_count { + let tx_type_byte = *read(1)?.first()?; + let tx_type = TransactionType::from_u8(tx_type_byte)?; + + let mut to = [0u8; 20]; + to.copy_from_slice(read(20)?); + let mut from = [0u8; 20]; + from.copy_from_slice(read(20)?); + + let data_len_bytes = read(4)?; + let data_len = u32::from_le_bytes(data_len_bytes.try_into().ok()?) as usize; + let data = read(data_len)?.to_vec(); + + let value_bytes = read(8)?; + let value = u64::from_le_bytes(value_bytes.try_into().ok()?); + + let nonce_bytes = read(8)?; + let nonce = u64::from_le_bytes(nonce_bytes.try_into().ok()?); + + transactions.push(Transaction { + tx_type, + to: Address(to), + from: Address(from), + data, + value, + nonce, + }); + } + + Some(TransactionBundle { transactions }) + } +} diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index dbf3f2a..1a3e889 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -151,7 +151,6 @@ impl VM { addr, data.len() ); - println!("📦 data written to 0x{:08x}: {:02x?}", addr, data); addr } From eb066736ba7116c9b085d1295636eb4a27067940 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 16 Dec 2025 21:48:36 +0100 Subject: [PATCH 08/70] Move default syscall handler to os and add os tests --- Cargo.lock | 1 + Makefile | 1 + crates/avm/Cargo.toml | 3 +- crates/avm/src/avm.rs | 8 +- crates/avm/tests/allocator_test.rs | 3 +- crates/os/src/bootloader.rs | 26 +- crates/os/src/lib.rs | 7 +- crates/os/src/syscalls.rs | 739 ++++++++++++++++++++++++++ crates/os/src/traps.rs | 93 ---- crates/os/tests/allocator_test.rs | 147 +++++ crates/os/tests/memory_page_offset.rs | 84 +++ crates/vm/src/sys_call.rs | 732 +------------------------ crates/vm/src/vm.rs | 27 +- 13 files changed, 1005 insertions(+), 866 deletions(-) create mode 100644 crates/os/src/syscalls.rs delete mode 100644 crates/os/src/traps.rs create mode 100644 crates/os/tests/allocator_test.rs create mode 100644 crates/os/tests/memory_page_offset.rs diff --git a/Cargo.lock b/Cargo.lock index 1a41d79..c988648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ version = "0.1.0" dependencies = [ "compiler", "hex", + "os", "state", "storage", "types", diff --git a/Makefile b/Makefile index 65435ca..a6aa9ef 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ test: generate_abis cargo test -p types -p storage -p state -- --nocapture cargo test -p program -- --nocapture cargo test -p vm -- --nocapture + cargo test -p os -- --nocapture cargo test -p compiler -- --nocapture cd crates/examples && cargo test -- --nocapture @echo "=== Tests complete ===" diff --git a/crates/avm/Cargo.toml b/crates/avm/Cargo.toml index 6e197be..ada2c94 100644 --- a/crates/avm/Cargo.toml +++ b/crates/avm/Cargo.toml @@ -9,4 +9,5 @@ compiler = { path = "../compiler" } # adjust path as needed state = { path = "../state" } # adjust path as needed storage = { path = "../storage" } # adjust path as needed vm = { path = "../vm" } # adjust path as needed -types = { path = "../types" } # adjust path as needed \ No newline at end of file +types = { path = "../types" } # adjust path as needed +os = { path = "../os" } diff --git a/crates/avm/src/avm.rs b/crates/avm/src/avm.rs index 967a981..b2744df 100644 --- a/crates/avm/src/avm.rs +++ b/crates/avm/src/avm.rs @@ -18,6 +18,7 @@ use types::address::Address; use types::result::Result; use vm::registers::Register; use vm::vm::VM; +use os::DefaultSyscallHandler; /// Application Virtual Machine (AVM) - the main orchestrator for smart contract execution. /// @@ -421,7 +422,12 @@ impl AVM { // - Removes the need for lifetimes like &'a mut dyn HostInterface // - Enables recursive call_contract logic, since the Box owns the host and doesn't borrow `self` // Without Box, we would need to track lifetimes manually and would hit borrow checker issues. - let mut vm: VM = VM::new_with_writer(memory_page, storage.clone(), Box::new(shim), self.verbose_writer.clone()); + let mut vm: VM = VM::new( + memory_page, + storage.clone(), + Box::new(shim), + Box::new(DefaultSyscallHandler::with_writer(self.verbose_writer.clone())), + ); let shared_meter = SharedGasMeter::new(Rc::clone(&self.gas_meter)); vm.set_metering(Box::new(shared_meter)); vm.set_code(0, Config::PROGRAM_START_ADDR, &account.code); diff --git a/crates/avm/tests/allocator_test.rs b/crates/avm/tests/allocator_test.rs index 4bbd732..3a8497c 100644 --- a/crates/avm/tests/allocator_test.rs +++ b/crates/avm/tests/allocator_test.rs @@ -5,7 +5,8 @@ use storage::Storage; use vm::host_interface; use vm::metering::NoopMeter; use vm::memory::Memory; -use vm::sys_call::{DefaultSyscallHandler, SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; +use os::DefaultSyscallHandler; +use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { diff --git a/crates/os/src/bootloader.rs b/crates/os/src/bootloader.rs index 0b4235f..3475e78 100644 --- a/crates/os/src/bootloader.rs +++ b/crates/os/src/bootloader.rs @@ -7,7 +7,7 @@ use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; use crate::memory::StackedMemory; -use crate::traps::{TrapAction, TrapFrame, TrapHandler, TrapTable}; +use crate::DefaultSyscallHandler; use storage::Storage; use vm::host_interface::NoopHost; use vm::memory::{Memory, HEAP_PTR_OFFSET}; @@ -27,12 +27,11 @@ impl Default for BootConfig { } /// Bootloader skeleton that loads a kernel image into fresh memory and -/// mediates syscall traps until the kernel installs its own handlers. +/// hands control to the kernel. #[derive(Debug)] pub struct Bootloader { pub config: BootConfig, memory: StackedMemory, - traps: TrapTable, } impl Bootloader { @@ -40,15 +39,9 @@ impl Bootloader { Self { config: BootConfig::default(), memory: StackedMemory::new(max_pages, page_size), - traps: TrapTable::new(), } } - /// Register a syscall trap handler that will run before transferring control to the kernel. - pub fn register_syscall_trap(&mut self, syscall_id: u32, handler: TrapHandler) { - self.traps.register(syscall_id, handler); - } - /// Load an ELF kernel image into a fresh page and return its entry point + backing memory. pub fn load_kernel(&mut self, elf_bytes: &[u8]) -> (u32, Memory) { let elf = parse_elf_from_bytes(elf_bytes).expect("failed to parse kernel ELF"); @@ -90,11 +83,6 @@ impl Bootloader { (entry_point, page) } - /// Dispatch a syscall trap using the boot-time trap table. - pub fn handle_trap(&self, frame: &mut TrapFrame) -> TrapAction { - self.traps.dispatch(frame) - } - /// Execute a transaction bundle by delegating to the kernel. This mirrors the /// AVM entry point where the kernel is responsible for invoking programs. pub fn execute_bundle(&mut self, kernel_elf: &[u8], bundle: &TransactionBundle) { @@ -102,12 +90,16 @@ impl Bootloader { let storage = Rc::new(RefCell::new(Storage::new())); let host: Box = Box::new(NoopHost); - let mut vm = VM::new(memory.clone(), storage, host); + let mut vm = VM::new( + memory.clone(), + storage, + host, + Box::new(DefaultSyscallHandler::new()), + ); self.place_bundle(&mut vm, bundle); vm.cpu.pc = entry_point; - // TODO: write the bundle into memory for the kernel to consume, and - // extend VM host to forward syscalls into the trap table. + // TODO: write the bundle into memory for the kernel to consume. vm.raw_run(); } diff --git a/crates/os/src/lib.rs b/crates/os/src/lib.rs index 0a03472..dd220b9 100644 --- a/crates/os/src/lib.rs +++ b/crates/os/src/lib.rs @@ -3,7 +3,6 @@ //! //! This crate provides a bootloader skeleton that: //! - loads a kernel program into fresh pages, -//! - wires a simple syscall trap table, and //! - hands the loaded image off to a future kernel runtime. //! //! Memory utilities are local copies of the VM page primitives to keep the OS @@ -16,4 +15,8 @@ pub mod bootloader; #[cfg(not(target_arch = "riscv32"))] pub mod memory; #[cfg(not(target_arch = "riscv32"))] -pub mod traps; +pub use vm::sys_call; +#[cfg(not(target_arch = "riscv32"))] +pub mod syscalls; +#[cfg(not(target_arch = "riscv32"))] +pub use syscalls::DefaultSyscallHandler; diff --git a/crates/os/src/syscalls.rs b/crates/os/src/syscalls.rs new file mode 100644 index 0000000..c116b47 --- /dev/null +++ b/crates/os/src/syscalls.rs @@ -0,0 +1,739 @@ +use core::cell::RefCell; +use core::fmt::Write; +use std::any::Any; +use std::rc::Rc; + +use storage::Storage; +use types::result::RESULT_SIZE; +use vm::host_interface::HostInterface; +use vm::memory::{Memory, HEAP_PTR_OFFSET}; +use vm::metering::{MeterResult, Metering}; +use vm::registers::Register; +use vm::sys_call::{ + SyscallHandler, SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, + SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, + SYSCALL_TRANSFER, +}; +/// Represents different types of arguments that can be passed to system calls. +/// +/// EDUCATIONAL: This enum demonstrates how to handle different data types +/// in system calls. In real operating systems, system calls need to handle +/// various data types safely. +#[allow(dead_code)] +enum Arg { + U32(u32), // 32-bit unsigned integer + F32(f32), // 32-bit floating point + Char(char), // Single character + Str(String), // String (owned) + Bytes(Vec), // Raw bytes +} + +pub struct DefaultSyscallHandler { + verbose_writer: Option>>, +} + +impl std::fmt::Debug for DefaultSyscallHandler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultSyscallHandler") + .field( + "verbose_writer", + &self.verbose_writer.as_ref().map(|_| ""), + ) + .finish() + } +} + +impl DefaultSyscallHandler { + pub fn new() -> Self { + Self { + verbose_writer: None, + } + } + + pub fn with_writer(writer: Option>>) -> Self { + Self { + verbose_writer: writer, + } + } +} + +impl SyscallHandler for DefaultSyscallHandler { + fn handle_syscall( + &mut self, + call_id: u32, + args: [u32; 6], + memory: Memory, + storage: Rc>, + host: &mut Box, + regs: &mut [u32; 32], + metering: &mut dyn Metering, + ) -> (u32, bool) { + if matches!(metering.on_syscall(call_id, &args), MeterResult::Halt) { + panic!("Metering halted syscall {}", call_id); + } + let result = match call_id { + SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, storage, metering), + SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, storage, metering), + SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), + SYSCALL_LOG => self.sys_log(args, memory, metering), + SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host, metering), + SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host, metering), + SYSCALL_ALLOC => self.sys_alloc(args, memory, metering), + SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), + SYSCALL_TRANSFER => self.sys_transfer(args, memory, host, metering), + SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), + _ => { + panic!("Unknown syscall: {}", call_id); + } + }; + (result, true) + } + fn as_any(&self) -> &dyn Any { + self + } +} + +impl DefaultSyscallHandler { + pub fn sys_fire_event( + &mut self, + args: [u32; 6], + memory: Memory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { + // EDUCATIONAL: Extract key pointer and length from arguments + let ptr = args[0] as usize; + let len = args[1] as usize; + + if matches!( + metering.on_syscall_data(SYSCALL_FIRE_EVENT, len), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_FIRE_EVENT"); + } + + let borrowed_memory = memory.as_ref(); + + // EDUCATIONAL: Safely read the key from memory + // EDUCATIONAL: Create a limited scope to avoid borrow checker issues + let event_bytes = match borrowed_memory.mem_slice(ptr, ptr + len) { + Some(r) => r, + None => panic!("invalid memory access"), // Invalid memory access + }; + + host.fire_event(event_bytes.to_vec()); + 0 + } + + fn sys_storage_get( + &mut self, + args: [u32; 6], + memory: Memory, + storage: Rc>, + metering: &mut dyn Metering, + ) -> u32 { + let domain_ptr = args[0] as usize; + let domain_len = args[1] as usize; + let key_ptr = args[2] as usize; + let key_len = args[3] as usize; + + let total_len = domain_len.saturating_add(key_len); + if matches!( + metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_STORAGE_GET"); + } + + let borrowed_memory = memory.as_ref(); + + // Parse domain + let domain_slice = { + let domain_slice_ref = + match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { + Some(r) => r, + None => { + println!( + "❌ Storage GET - Invalid domain memory access: ptr={}, len={}", + domain_ptr, domain_len + ); + return 0; + } + }; + domain_slice_ref.as_ref().to_vec() + }; + let domain = match core::str::from_utf8(&domain_slice) { + Ok(s) => s, + Err(_) => { + println!( + "❌ Storage GET - Invalid UTF-8 in domain: {:?}", + domain_slice + ); + return 0; + } + }; + + // Parse key + let key_slice = { + let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { + Some(r) => r, + None => { + println!( + "❌ Storage GET - Invalid key memory access: ptr={}, len={}", + key_ptr, key_len + ); + return 0; + } + }; + key_slice_ref.as_ref().to_vec() + }; + // Convert binary key to hex string for storage + let key = key_slice + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + + // Format key for display based on domain + let display_key = if domain == "P" { + // For persistent domain, try to display key as ASCII + match core::str::from_utf8(&key_slice) { + Ok(s) => s.to_string(), + Err(_) => key.clone(), // fallback to hex if not valid UTF-8 + } + } else { + // For other domains, show domain as ASCII and key as hex + format!("{}:{}", domain, key) + }; + + if let Some(value) = storage.borrow().get(domain, &key) { + let mut buf = (value.len() as u32).to_le_bytes().to_vec(); + buf.extend_from_slice(value.as_slice()); + if matches!(metering.on_alloc(buf.len()), MeterResult::Halt) { + panic!("Metering halted alloc during storage_get"); + } + let addr = borrowed_memory.alloc_on_heap(&buf); + println!( + "✅ Found value for domain: '{}', Key: '{}'", + domain, display_key + ); + return addr; + } else { + println!( + "❌ No value found for domain: '{}', key: '{}'", + domain, display_key + ); + 0 + } + } + + fn sys_storage_set( + &mut self, + args: [u32; 6], + memory: Memory, + storage: Rc>, + metering: &mut dyn Metering, + ) -> u32 { + let domain_ptr = args[0] as usize; + let domain_len = args[1] as usize; + let key_ptr = args[2] as usize; + let key_len = args[3] as usize; + let val_ptr = args[4] as usize; + let val_len = args[5] as usize; + + let total_len = domain_len.saturating_add(key_len).saturating_add(val_len); + if matches!( + metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_STORAGE_SET"); + } + + let borrowed_memory = memory.as_ref(); + + // Parse domain + let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) + { + Some(r) => r, + None => { + println!( + "❌ Storage SET - Invalid domain memory access: ptr={}, len={}", + domain_ptr, domain_len + ); + return 0; + } + }; + let domain_slice = domain_slice_ref.as_ref(); + let domain = match core::str::from_utf8(domain_slice) { + Ok(s) => s, + Err(_) => { + println!( + "❌ Storage SET - Invalid UTF-8 in domain: {:?}", + domain_slice + ); + return 0; + } + }; + + // Parse key + let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { + Some(r) => r, + None => { + println!( + "❌ Storage SET - Invalid key memory access: ptr={}, len={}", + key_ptr, key_len + ); + return 0; + } + }; + let key_slice = key_slice_ref.as_ref(); + // Convert binary key to hex string for storage + let key = key_slice + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + + // Format key for display based on domain + let display_key = if domain == "P" { + // For persistent domain, try to display key as ASCII + match core::str::from_utf8(key_slice) { + Ok(s) => s.to_string(), + Err(_) => key.clone(), // fallback to hex if not valid UTF-8 + } + } else { + // For other domains, show domain as ASCII and key as hex + format!("{}:{}", domain, key) + }; + + // Parse value + let value_slice_ref = match borrowed_memory.mem_slice(val_ptr, val_ptr + val_len) { + Some(r) => r, + None => { + println!( + "❌ Storage SET - Invalid value memory access: ptr={}, len={}", + val_ptr, val_len + ); + return 0; + } + }; + let value_slice = value_slice_ref.as_ref(); + + println!( + "💾 Storage SET - Domain: '{}', Key: '{}', Value: {:?} ({} bytes)", + domain, + display_key, + value_slice, + value_slice.len() + ); + + storage.borrow_mut().set(domain, &key, value_slice.to_vec()); + 0 + } + + fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Memory) -> u32 { + let msg_ptr = regs[Register::A0 as usize] as usize; + let msg_len = regs[Register::A1 as usize] as usize; + let msg = memory + .mem_slice(msg_ptr, msg_ptr + msg_len) + .map(|bytes| String::from_utf8_lossy(bytes.as_ref()).into_owned()) + .unwrap_or_else(|| "".to_string()); + panic!("🔥 Guest panic: {}", msg); + } + + fn sys_log( + &mut self, + args: [u32; 6], + memory: Memory, + metering: &mut dyn Metering, + ) -> u32 { + let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; + let payload_len = fmt_len.saturating_add(arg_len) as usize; + if matches!( + metering.on_syscall_data(SYSCALL_LOG, payload_len), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_LOG"); + } + let borrowed_memory = memory.as_ref(); + let fmt_slice = + match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { + Some(s) => s, + None => { + println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); + return 0; + } + }; + let fmt_bytes = fmt_slice.as_ref(); + let fmt = match core::str::from_utf8(fmt_bytes) { + Ok(s) => s, + Err(e) => { + println!("⚠️ invalid UTF-8 in format string"); + println!("📦 bytes: {:?}", fmt_bytes); + println!("❌ error: {}", e); + return 0; + } + }; + let args_bytes_slice = + borrowed_memory.mem_slice(arg_ptr as usize, (arg_ptr + arg_len) as usize); + let args_bytes_holder; + let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { + args_bytes_holder = slice; + args_bytes_holder.as_ref() + } else { + b"" + }; + let raw_args: Vec = args_bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(); + let mut args: Vec = Vec::new(); + let mut raw_iter = raw_args.into_iter(); + let mut chars = fmt.chars().peekable(); + while let Some(c) = chars.next() { + if c != '%' { + continue; + } + let spec: char = chars.next().unwrap_or('%'); + let mut next = || raw_iter.next().unwrap_or(0); + match spec { + 'd' | 'u' | 'x' => args.push(Arg::U32(next())), + 'f' => args.push(Arg::F32(f32::from_bits(next()))), + 'c' => args.push(Arg::Char(char::from_u32(next()).unwrap_or('?'))), + 's' => { + let ptr = next() as usize; + let len = next() as usize; + match borrowed_memory.mem_slice(ptr, ptr + len) { + Some(slice) => { + let s_ptr = core::str::from_utf8(slice.as_ref()); + args.push(match s_ptr { + Ok(s) => Arg::Str(s.to_string()), + Err(_) => Arg::Str("".to_string()), + }); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'b' => { + let ptr = next() as usize; + let len = next() as usize; + match borrowed_memory.mem_slice(ptr, ptr + len) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'a' => { + // Array of u32s + let ptr = next() as usize; + let len = next() as usize; + let byte_len = len * 4; // u32 is 4 bytes + match borrowed_memory.mem_slice(ptr, ptr + byte_len) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'A' => { + // Array of u8s + let ptr = next() as usize; + let len = next() as usize; + match borrowed_memory.mem_slice(ptr, ptr + len) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + _ => args.push(Arg::Str("".to_string())), + } + } + let mut output = String::new(); + let mut args_iter = args.iter(); + let mut fmt_chars = fmt.chars().peekable(); + while let Some(c) = fmt_chars.next() { + if c == '%' { + match fmt_chars.next() { + Some('d') | Some('u') => match args_iter.next() { + Some(Arg::U32(v)) => output.push_str(&format!("{}", *v as i32)), + _ => output.push_str(""), + }, + Some('x') => match args_iter.next() { + Some(Arg::U32(v)) => output.push_str(&format!("{:08x}", v)), + _ => output.push_str(""), + }, + Some('f') => match args_iter.next() { + Some(Arg::F32(f)) => output.push_str(&format!("{}", f)), + _ => output.push_str(""), + }, + Some('c') => match args_iter.next() { + Some(Arg::Char(c)) => output.push(*c), + _ => output.push_str(""), + }, + Some('s') => match args_iter.next() { + Some(Arg::Str(s)) => output.push_str(s), + _ => output.push_str(""), + }, + Some('b') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + // Format bytes array nicely + output.push('['); + for (i, byte) in b.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(&format!("0x{:02x}", byte)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('a') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + // Format u32 array (bytes interpreted as u32s) + output.push('['); + for (i, chunk) in b.chunks_exact(4).enumerate() { + if i > 0 { + output.push_str(", "); + } + let val = + u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + output.push_str(&format!("{}", val)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('A') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + // Format u8 array + output.push('['); + for (i, byte) in b.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(&format!("{}", byte)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('%') => output.push('%'), + Some(_) | None => output.push_str("<%?>"), + } + } else { + output.push(c); + } + } + match &self.verbose_writer { + Some(writer) => { + let _ = writeln!(writer.borrow_mut(), "📜 Guest Log: {}", output); + } + None => { + println!("📜 Guest Log: {}", output); + } + } + 0 + } + + fn sys_call_program( + &mut self, + args: [u32; 6], + memory: Memory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { + let to_ptr = args[0] as usize; + let from_ptr = args[1] as usize; + let input_ptr = args[2] as usize; + let input_len = args[3] as usize; + let result_ptr: u32; + let page_index: usize; + if matches!(metering.on_call(input_len), MeterResult::Halt) { + panic!("Metering halted SYSCALL_CALL_PROGRAM"); + } + { + let borrowed_memory = memory.as_ref(); + let to_slice = match borrowed_memory.mem_slice(to_ptr, to_ptr + 20) { + Some(r) => r, + None => return 0, + }; + let from_slice = match borrowed_memory.mem_slice(from_ptr, from_ptr + 20) { + Some(r) => r, + None => return 0, + }; + let input_slice = match borrowed_memory.mem_slice(input_ptr, input_ptr + input_len) { + Some(r) => r, + None => return 0, + }; + let mut to_bytes = [0u8; 20]; + let mut from_bytes = [0u8; 20]; + to_bytes.copy_from_slice(&to_slice); + from_bytes.copy_from_slice(&from_slice); + let input_vec = input_slice.to_vec(); + (result_ptr, page_index) = host.call_program(from_bytes, to_bytes, input_vec); + } + { + let borrowed_memory = memory.as_ref(); + let result_bytes = match host.read_memory_page(page_index, result_ptr, RESULT_SIZE) { + Some(b) => b, + None => return 0, + }; + if matches!(metering.on_alloc(result_bytes.len()), MeterResult::Halt) { + panic!("Metering halted alloc for call_program result"); + } + borrowed_memory.alloc_on_heap(&result_bytes) + } + } + + fn sys_alloc( + &mut self, + args: [u32; 6], + memory: Memory, + metering: &mut dyn Metering, + ) -> u32 { + let size = args[0] as usize; // A0 register + let align = args[1] as usize; // A1 register + + if matches!(metering.on_alloc(size), MeterResult::Halt) { + panic!("Metering halted SYSCALL_ALLOC"); + } + + if size == 0 { + println!("VM Alloc: Invalid size 0"); + return 0; + } + + // Validate alignment (must be power of 2) + if align == 0 || (align & (align - 1)) != 0 { + println!("VM Alloc: Invalid alignment {}", align); + return 0; + } + + let current_heap = memory.next_heap(); + + // Initialize heap pointer if not set (no code has been written) + if current_heap == 0 { + memory.set_next_heap(HEAP_PTR_OFFSET); + } + + // Allocate aligned memory on heap + let data = vec![0u8; size]; + let ptr = memory.alloc_on_heap(&data); + + if ptr == 0 { + println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); + return 0; + } + + // Check if allocated address meets alignment requirements + if (ptr as usize) % align != 0 { + // Re-allocate with enough space for alignment + let total_size = size + align - 1; + let padded_data = vec![0u8; total_size]; + let padded_ptr = memory.alloc_on_heap(&padded_data); + if padded_ptr == 0 { + println!( + "VM Alloc: Out of memory, failed to allocate {} bytes for alignment", + total_size + ); + return 0; + } + // Return properly aligned pointer within the allocated region + let aligned_ptr = ((padded_ptr as usize + align - 1) & !(align - 1)) as u32; + return aligned_ptr; + } + + ptr + } + + fn sys_dealloc( + &mut self, + args: [u32; 6], + _memory: Memory, + metering: &mut dyn Metering, + ) -> u32 { + let size = args[1] as usize; + if matches!(metering.on_alloc(size), MeterResult::Halt) { + panic!("Metering halted SYSCALL_DEALLOC"); + } + // Note: This VM uses a simple bump allocator, so we can't actually free memory + // In a real VM, you'd implement a proper allocator with free lists + // For now, this is a no-op since the memory will be reclaimed when the VM exits + 0 + } + + fn sys_transfer( + &mut self, + args: [u32; 6], + memory: Memory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { + // args: a2=to ptr, a3=value_lo, a4=value_hi + let to_ptr = args[1] as usize; + let value_lo = args[2] as u64; + let value_hi = args[3] as u64; + let value = value_lo | (value_hi << 32); + + if matches!( + metering.on_syscall_data(SYSCALL_TRANSFER, 20), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_TRANSFER"); + } + + let borrowed = memory.as_ref(); + let to_slice = borrowed + .mem_slice(to_ptr, to_ptr + 20) + .expect("invalid to ptr"); + + let mut to = [0u8; 20]; + to.copy_from_slice(to_slice.as_ref()); + + if host.transfer(to, value) { + 0 + } else { + 1 + } + } + + fn sys_balance( + &mut self, + args: [u32; 6], + memory: Memory, + host: &mut Box, + metering: &mut dyn Metering, + ) -> u32 { + // args: a1 = address pointer (20 bytes) + let addr_ptr = args[0] as usize; + if matches!( + metering.on_syscall_data(SYSCALL_BALANCE, 20), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_BALANCE"); + } + let addr = { + let borrowed = memory.as_ref(); + let addr_slice = borrowed + .mem_slice(addr_ptr, addr_ptr + 20) + .expect("invalid addr ptr"); + let mut addr = [0u8; 20]; + addr.copy_from_slice(addr_slice.as_ref()); + addr + }; + + let bal = host.balance(addr); + memory.alloc_on_heap(&bal.to_le_bytes()) + } +} diff --git a/crates/os/src/traps.rs b/crates/os/src/traps.rs deleted file mode 100644 index e76da53..0000000 --- a/crates/os/src/traps.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::collections::HashMap; -use std::fmt; - -/// Outcome after a trap handler runs. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TrapAction { - Continue, - KillKernel, -} - -/// Lightweight snapshot of CPU state for a syscall trap. -#[derive(Debug, Clone)] -pub struct TrapFrame { - pub syscall_id: u32, - pub args: [u32; 6], - pub pc: usize, -} - -pub type TrapHandler = Box TrapAction + Send + Sync>; - -/// Syscall IDs mirrored from the VM syscall table. -pub mod syscall { - pub const STORAGE_GET: u32 = 1; - pub const STORAGE_SET: u32 = 2; - pub const PANIC: u32 = 3; - pub const LOG: u32 = 4; - pub const CALL_PROGRAM: u32 = 5; - pub const FIRE_EVENT: u32 = 6; - pub const ALLOC: u32 = 7; - pub const DEALLOC: u32 = 8; - pub const TRANSFER: u32 = 9; - pub const BALANCE: u32 = 10; -} - -/// Simple table that dispatches traps by syscall id. -#[derive(Default)] -pub struct TrapTable { - handlers: HashMap, -} - -impl fmt::Debug for TrapTable { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TrapTable") - .field("registered", &self.handlers.len()) - .finish() - } -} - -impl TrapTable { - pub fn new() -> Self { - let mut table = Self { - handlers: HashMap::new(), - }; - table.register_logging_stubs(); - table - } - - pub fn register(&mut self, syscall_id: u32, handler: TrapHandler) { - self.handlers.insert(syscall_id, handler); - } - - pub fn dispatch(&self, frame: &mut TrapFrame) -> TrapAction { - if let Some(handler) = self.handlers.get(&frame.syscall_id) { - handler(frame) - } else { - TrapAction::KillKernel - } - } - - /// Install a default stub for every VM syscall that just logs the call. - pub fn register_logging_stubs(&mut self) { - self.register(syscall::STORAGE_GET, log_stub("STORAGE_GET")); - self.register(syscall::STORAGE_SET, log_stub("STORAGE_SET")); - self.register(syscall::PANIC, log_stub("PANIC")); - self.register(syscall::LOG, log_stub("LOG")); - self.register(syscall::CALL_PROGRAM, log_stub("CALL_PROGRAM")); - self.register(syscall::FIRE_EVENT, log_stub("FIRE_EVENT")); - self.register(syscall::ALLOC, log_stub("ALLOC")); - self.register(syscall::DEALLOC, log_stub("DEALLOC")); - self.register(syscall::TRANSFER, log_stub("TRANSFER")); - self.register(syscall::BALANCE, log_stub("BALANCE")); - } -} - -fn log_stub(name: &'static str) -> TrapHandler { - Box::new(move |frame: &mut TrapFrame| { - println!( - "trap {} called (id={}, pc=0x{:08x}, args={:?})", - name, frame.syscall_id, frame.pc, frame.args - ); - TrapAction::Continue - }) -} diff --git a/crates/os/tests/allocator_test.rs b/crates/os/tests/allocator_test.rs new file mode 100644 index 0000000..6a4cbff --- /dev/null +++ b/crates/os/tests/allocator_test.rs @@ -0,0 +1,147 @@ +use os::memory::MemoryPage; +use os::DefaultSyscallHandler; +use std::cell::RefCell; +use std::rc::Rc; +use storage::Storage; +use vm::host_interface; +use vm::memory::Memory; +use vm::metering::NoopMeter; +use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; + +#[test] +fn test_allocator_syscalls() { + let memory: Memory = Rc::new(MemoryPage::new(8192)); + let storage = Rc::new(RefCell::new(Storage::new())); + let mut host: Box = Box::new(host_interface::NoopHost); + let mut syscall_handler = DefaultSyscallHandler::new(); + let mut meter = NoopMeter::default(); + + // Test SYSCALL_ALLOC + let args = [1024, 8, 0, 0, 0, 0]; + let mut regs = [0u32; 32]; + let (result, _) = syscall_handler.handle_syscall( + SYSCALL_ALLOC, + args, + memory.clone(), + storage.clone(), + &mut host, + &mut regs, + &mut meter, + ); + + assert_ne!(result, 0); + + // Test SYSCALL_DEALLOC (no-op but should not crash) + let dealloc_args = [result, 1024, 0, 0, 0, 0]; + let (dealloc_result, _) = syscall_handler.handle_syscall( + SYSCALL_DEALLOC, + dealloc_args, + memory.clone(), + storage.clone(), + &mut host, + &mut regs, + &mut meter, + ); + + assert_eq!(dealloc_result, 0); +} + +#[test] +fn test_multiple_allocations() { + let memory: Memory = Rc::new(MemoryPage::new(8192)); + let storage = Rc::new(RefCell::new(Storage::new())); + let mut host: Box = Box::new(host_interface::NoopHost); + let mut syscall_handler = DefaultSyscallHandler::new(); + let mut regs = [0u32; 32]; + let mut meter = NoopMeter::default(); + + let mut pointers = Vec::new(); + + // Allocate multiple blocks + for i in 0..5 { + let size = 64 + i * 32; + let args = [size, 4, 0, 0, 0, 0]; + + let (ptr, _) = syscall_handler.handle_syscall( + SYSCALL_ALLOC, + args, + memory.clone(), + storage.clone(), + &mut host, + &mut regs, + &mut meter, + ); + + assert_ne!(ptr, 0); + pointers.push(ptr); + } + + // Verify pointers are aligned + for &ptr in &pointers { + assert_eq!(ptr % 4, 0); + } + + // Verify no overlapping pointers (simple check) + for i in 0..pointers.len() { + for j in i + 1..pointers.len() { + assert_ne!(pointers[i], pointers[j]); + } + } +} + +#[test] +fn test_alignment_requirements() { + let memory: Memory = Rc::new(MemoryPage::new(8192)); + let storage = Rc::new(RefCell::new(Storage::new())); + let mut host: Box = Box::new(host_interface::NoopHost); + let mut syscall_handler = DefaultSyscallHandler::new(); + let mut regs = [0u32; 32]; + let mut meter = NoopMeter::default(); + + // Test various alignments + let alignments = [1, 2, 4, 8, 16]; + + for &align in &alignments { + let args = [256, align as u32, 0, 0, 0, 0]; + let (ptr, _) = syscall_handler.handle_syscall( + SYSCALL_ALLOC, + args, + memory.clone(), + storage.clone(), + &mut host, + &mut regs, + &mut meter, + ); + + assert_ne!(ptr, 0); + assert_eq!(ptr as usize % align, 0); + } +} + +#[test] +fn test_invalid_alignment() { + let memory: Memory = Rc::new(MemoryPage::new(8192)); + let storage = Rc::new(RefCell::new(Storage::new())); + let mut host: Box = Box::new(host_interface::NoopHost); + let mut syscall_handler = DefaultSyscallHandler::new(); + let mut regs = [0u32; 32]; + let mut meter = NoopMeter::default(); + + // Test invalid alignments (not powers of 2) + let invalid_alignments = [0, 3, 5, 6, 7, 9]; + + for &align in &invalid_alignments { + let args = [100, align as u32, 0, 0, 0, 0]; + let (ptr, _) = syscall_handler.handle_syscall( + SYSCALL_ALLOC, + args, + memory.clone(), + storage.clone(), + &mut host, + &mut regs, + &mut meter, + ); + + assert_eq!(ptr, 0); + } +} diff --git a/crates/os/tests/memory_page_offset.rs b/crates/os/tests/memory_page_offset.rs new file mode 100644 index 0000000..df10b9d --- /dev/null +++ b/crates/os/tests/memory_page_offset.rs @@ -0,0 +1,84 @@ +use os::memory::MemoryPage; +use vm::metering::{MemoryAccessKind, NoopMeter}; + +#[test] +fn test_offset_zero_base() { + let mem = MemoryPage::new_with_base(1024, 0); + let mut meter = NoopMeter::default(); + assert_eq!(mem.offset(0), 0); + assert_eq!(mem.offset(100), 100); + assert_eq!(mem.offset(1023), 1023); + assert_eq!(mem.load_u32(0, &mut meter, MemoryAccessKind::Load), Some(0)); +} + +#[test] +fn test_offset_high_base() { + let base = 0x8000_0000; + let mem = MemoryPage::new_with_base(1024, base); + assert_eq!(mem.offset(base), 0); + assert_eq!(mem.offset(base + 100), 100); + assert_eq!(mem.offset(base + 1023), 1023); +} + +#[test] +#[should_panic(expected = "Address below base_address")] +fn test_offset_below_base_panics() { + let base = 0x8000_0000; + let mem = MemoryPage::new_with_base(1024, base); + mem.offset(base - 1); +} + +#[test] +fn test_store_and_load_zero_base() { + let mem = MemoryPage::new_with_base(1024, 0); + let mut meter = NoopMeter::default(); + assert!(mem.store_u8(10, 0xAB, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_byte(10, &mut meter, MemoryAccessKind::Load), + Some(0xAB) + ); + assert!(mem.store_u16(20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_halfword(20, &mut meter, MemoryAccessKind::Load), + Some(0xCDEF) + ); + assert!(mem.store_u32(30, 0x1234_5678, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_u32(30, &mut meter, MemoryAccessKind::Load), + Some(0x1234_5678) + ); +} + +#[test] +fn test_store_and_load_high_base() { + let base = 0x8000_0000; + let mem = MemoryPage::new_with_base(1024, base); + let mut meter = NoopMeter::default(); + assert!(mem.store_u8(base + 10, 0xAB, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_byte(base + 10, &mut meter, MemoryAccessKind::Load), + Some(0xAB) + ); + assert!(mem.store_u16(base + 20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_halfword(base + 20, &mut meter, MemoryAccessKind::Load), + Some(0xCDEF) + ); + assert!(mem.store_u32(base + 30, 0x1234_5678, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_u32(base + 30, &mut meter, MemoryAccessKind::Load), + Some(0x1234_5678) + ); +} + +#[test] +fn test_store_and_load_at_offset_zero() { + let base = 0x8000_0000; + let mem = MemoryPage::new_with_base(1024, base); + let mut meter = NoopMeter::default(); + assert!(mem.store_u8(base, 0xAA, &mut meter, MemoryAccessKind::Store)); + assert_eq!( + mem.load_byte(base, &mut meter, MemoryAccessKind::Load), + Some(0xAA) + ); +} diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index d5ae6f0..a723b8e 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -1,13 +1,10 @@ use crate::host_interface::HostInterface; -use crate::memory::{Memory, HEAP_PTR_OFFSET}; -use crate::metering::{MeterResult, Metering}; -use crate::registers::Register; +use crate::memory::Memory; +use crate::metering::Metering; +use core::any::Any; use core::cell::RefCell; -use core::fmt::Write; -use std::any::Any; use std::rc::Rc; use storage::Storage; -use types::result::RESULT_SIZE; /// System call IDs for the VM. pub const SYSCALL_STORAGE_GET: u32 = 1; @@ -20,19 +17,8 @@ pub const SYSCALL_ALLOC: u32 = 7; pub const SYSCALL_DEALLOC: u32 = 8; pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; -/// Represents different types of arguments that can be passed to system calls. -/// -/// EDUCATIONAL: This enum demonstrates how to handle different data types -/// in system calls. In real operating systems, system calls need to handle -/// various data types safely. -enum Arg { - U32(u32), // 32-bit unsigned integer - F32(f32), // 32-bit floating point - Char(char), // Single character - Str(String), // String (owned) - Bytes(Vec), // Raw bytes -} +/// Trait implemented by syscall handlers consumed by the VM. pub trait SyscallHandler: std::fmt::Debug { fn handle_syscall( &mut self, @@ -46,713 +32,3 @@ pub trait SyscallHandler: std::fmt::Debug { ) -> (u32, bool); fn as_any(&self) -> &dyn Any; } - -pub struct DefaultSyscallHandler { - verbose_writer: Option>>, -} - -impl std::fmt::Debug for DefaultSyscallHandler { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DefaultSyscallHandler") - .field( - "verbose_writer", - &self.verbose_writer.as_ref().map(|_| ""), - ) - .finish() - } -} - -impl DefaultSyscallHandler { - pub fn new() -> Self { - Self { - verbose_writer: None, - } - } - - pub fn with_writer(writer: Option>>) -> Self { - Self { - verbose_writer: writer, - } - } -} - -impl SyscallHandler for DefaultSyscallHandler { - fn handle_syscall( - &mut self, - call_id: u32, - args: [u32; 6], - memory: Memory, - storage: Rc>, - host: &mut Box, - regs: &mut [u32; 32], - metering: &mut dyn Metering, - ) -> (u32, bool) { - if matches!(metering.on_syscall(call_id, &args), MeterResult::Halt) { - panic!("Metering halted syscall {}", call_id); - } - let result = match call_id { - SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, storage, metering), - SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, storage, metering), - SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), - SYSCALL_LOG => self.sys_log(args, memory, metering), - SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host, metering), - SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host, metering), - SYSCALL_ALLOC => self.sys_alloc(args, memory, metering), - SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), - SYSCALL_TRANSFER => self.sys_transfer(args, memory, host, metering), - SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), - _ => { - panic!("Unknown syscall: {}", call_id); - } - }; - (result, true) - } - fn as_any(&self) -> &dyn Any { - self - } -} - -impl DefaultSyscallHandler { - pub fn sys_fire_event( - &mut self, - args: [u32; 6], - memory: Memory, - host: &mut Box, - metering: &mut dyn Metering, - ) -> u32 { - // EDUCATIONAL: Extract key pointer and length from arguments - let ptr = args[0] as usize; - let len = args[1] as usize; - - if matches!( - metering.on_syscall_data(SYSCALL_FIRE_EVENT, len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_FIRE_EVENT"); - } - - let borrowed_memory = memory.as_ref(); - - // EDUCATIONAL: Safely read the key from memory - // EDUCATIONAL: Create a limited scope to avoid borrow checker issues - let event_bytes = match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(r) => r, - None => panic!("invalid memory access"), // Invalid memory access - }; - - host.fire_event(event_bytes.to_vec()); - 0 - } - - fn sys_storage_get( - &mut self, - args: [u32; 6], - memory: Memory, - storage: Rc>, - metering: &mut dyn Metering, - ) -> u32 { - let domain_ptr = args[0] as usize; - let domain_len = args[1] as usize; - let key_ptr = args[2] as usize; - let key_len = args[3] as usize; - - let total_len = domain_len.saturating_add(key_len); - if matches!( - metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_STORAGE_GET"); - } - - let borrowed_memory = memory.as_ref(); - - // Parse domain - let domain_slice = { - let domain_slice_ref = - match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid domain memory access: ptr={}, len={}", - domain_ptr, domain_len - ); - return 0; - } - }; - domain_slice_ref.as_ref().to_vec() - }; - let domain = match core::str::from_utf8(&domain_slice) { - Ok(s) => s, - Err(_) => { - println!( - "❌ Storage GET - Invalid UTF-8 in domain: {:?}", - domain_slice - ); - return 0; - } - }; - - // Parse key - let key_slice = { - let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid key memory access: ptr={}, len={}", - key_ptr, key_len - ); - return 0; - } - }; - key_slice_ref.as_ref().to_vec() - }; - // Convert binary key to hex string for storage - let key = key_slice - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); - - // Format key for display based on domain - let display_key = if domain == "P" { - // For persistent domain, try to display key as ASCII - match core::str::from_utf8(&key_slice) { - Ok(s) => s.to_string(), - Err(_) => key.clone(), // fallback to hex if not valid UTF-8 - } - } else { - // For other domains, show domain as ASCII and key as hex - format!("{}:{}", domain, key) - }; - - if let Some(value) = storage.borrow().get(domain, &key) { - let mut buf = (value.len() as u32).to_le_bytes().to_vec(); - buf.extend_from_slice(value.as_slice()); - if matches!(metering.on_alloc(buf.len()), MeterResult::Halt) { - panic!("Metering halted alloc during storage_get"); - } - let addr = borrowed_memory.alloc_on_heap(&buf); - println!( - "✅ Found value for domain: '{}', Key: '{}'", - domain, display_key - ); - return addr; - } else { - println!( - "❌ No value found for domain: '{}', key: '{}'", - domain, display_key - ); - 0 - } - } - - fn sys_storage_set( - &mut self, - args: [u32; 6], - memory: Memory, - storage: Rc>, - metering: &mut dyn Metering, - ) -> u32 { - let domain_ptr = args[0] as usize; - let domain_len = args[1] as usize; - let key_ptr = args[2] as usize; - let key_len = args[3] as usize; - let val_ptr = args[4] as usize; - let val_len = args[5] as usize; - - let total_len = domain_len.saturating_add(key_len).saturating_add(val_len); - if matches!( - metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_STORAGE_SET"); - } - - let borrowed_memory = memory.as_ref(); - - // Parse domain - let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) - { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid domain memory access: ptr={}, len={}", - domain_ptr, domain_len - ); - return 0; - } - }; - let domain_slice = domain_slice_ref.as_ref(); - let domain = match core::str::from_utf8(domain_slice) { - Ok(s) => s, - Err(_) => { - println!( - "❌ Storage SET - Invalid UTF-8 in domain: {:?}", - domain_slice - ); - return 0; - } - }; - - // Parse key - let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid key memory access: ptr={}, len={}", - key_ptr, key_len - ); - return 0; - } - }; - let key_slice = key_slice_ref.as_ref(); - // Convert binary key to hex string for storage - let key = key_slice - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); - - // Format key for display based on domain - let display_key = if domain == "P" { - // For persistent domain, try to display key as ASCII - match core::str::from_utf8(key_slice) { - Ok(s) => s.to_string(), - Err(_) => key.clone(), // fallback to hex if not valid UTF-8 - } - } else { - // For other domains, show domain as ASCII and key as hex - format!("{}:{}", domain, key) - }; - - // Parse value - let value_slice_ref = match borrowed_memory.mem_slice(val_ptr, val_ptr + val_len) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid value memory access: ptr={}, len={}", - val_ptr, val_len - ); - return 0; - } - }; - let value_slice = value_slice_ref.as_ref(); - - println!( - "💾 Storage SET - Domain: '{}', Key: '{}', Value: {:?} ({} bytes)", - domain, - display_key, - value_slice, - value_slice.len() - ); - - storage.borrow_mut().set(domain, &key, value_slice.to_vec()); - 0 - } - - fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Memory) -> u32 { - let msg_ptr = regs[Register::A0 as usize] as usize; - let msg_len = regs[Register::A1 as usize] as usize; - let msg = memory - .mem_slice(msg_ptr, msg_ptr + msg_len) - .map(|bytes| String::from_utf8_lossy(bytes.as_ref()).into_owned()) - .unwrap_or_else(|| "".to_string()); - panic!("🔥 Guest panic: {}", msg); - } - - fn sys_log( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; - let payload_len = fmt_len.saturating_add(arg_len) as usize; - if matches!( - metering.on_syscall_data(SYSCALL_LOG, payload_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_LOG"); - } - let borrowed_memory = memory.as_ref(); - let fmt_slice = - match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { - Some(s) => s, - None => { - println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); - return 0; - } - }; - let fmt_bytes = fmt_slice.as_ref(); - let fmt = match core::str::from_utf8(fmt_bytes) { - Ok(s) => s, - Err(e) => { - println!("⚠️ invalid UTF-8 in format string"); - println!("📦 bytes: {:?}", fmt_bytes); - println!("❌ error: {}", e); - return 0; - } - }; - let args_bytes_slice = - borrowed_memory.mem_slice(arg_ptr as usize, (arg_ptr + arg_len) as usize); - let args_bytes_holder; - let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { - args_bytes_holder = slice; - args_bytes_holder.as_ref() - } else { - b"" - }; - let raw_args: Vec = args_bytes - .chunks_exact(4) - .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) - .collect(); - let mut args: Vec = Vec::new(); - let mut raw_iter = raw_args.into_iter(); - let mut chars = fmt.chars().peekable(); - while let Some(c) = chars.next() { - if c != '%' { - continue; - } - let spec: char = chars.next().unwrap_or('%'); - let mut next = || raw_iter.next().unwrap_or(0); - match spec { - 'd' | 'u' | 'x' => args.push(Arg::U32(next())), - 'f' => args.push(Arg::F32(f32::from_bits(next()))), - 'c' => args.push(Arg::Char(char::from_u32(next()).unwrap_or('?'))), - 's' => { - let ptr = next() as usize; - let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(slice) => { - let s_ptr = core::str::from_utf8(slice.as_ref()); - args.push(match s_ptr { - Ok(s) => Arg::Str(s.to_string()), - Err(_) => Arg::Str("".to_string()), - }); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'b' => { - let ptr = next() as usize; - let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'a' => { - // Array of u32s - let ptr = next() as usize; - let len = next() as usize; - let byte_len = len * 4; // u32 is 4 bytes - match borrowed_memory.mem_slice(ptr, ptr + byte_len) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'A' => { - // Array of u8s - let ptr = next() as usize; - let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - _ => args.push(Arg::Str("".to_string())), - } - } - let mut output = String::new(); - let mut args_iter = args.iter(); - let mut fmt_chars = fmt.chars().peekable(); - while let Some(c) = fmt_chars.next() { - if c == '%' { - match fmt_chars.next() { - Some('d') | Some('u') => match args_iter.next() { - Some(Arg::U32(v)) => output.push_str(&format!("{}", *v as i32)), - _ => output.push_str(""), - }, - Some('x') => match args_iter.next() { - Some(Arg::U32(v)) => output.push_str(&format!("{:08x}", v)), - _ => output.push_str(""), - }, - Some('f') => match args_iter.next() { - Some(Arg::F32(f)) => output.push_str(&format!("{}", f)), - _ => output.push_str(""), - }, - Some('c') => match args_iter.next() { - Some(Arg::Char(c)) => output.push(*c), - _ => output.push_str(""), - }, - Some('s') => match args_iter.next() { - Some(Arg::Str(s)) => output.push_str(s), - _ => output.push_str(""), - }, - Some('b') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format bytes array nicely - output.push('['); - for (i, byte) in b.iter().enumerate() { - if i > 0 { - output.push_str(", "); - } - output.push_str(&format!("0x{:02x}", byte)); - } - output.push(']'); - } - _ => output.push_str(""), - }, - Some('a') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format u32 array (bytes interpreted as u32s) - output.push('['); - for (i, chunk) in b.chunks_exact(4).enumerate() { - if i > 0 { - output.push_str(", "); - } - let val = - u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - output.push_str(&format!("{}", val)); - } - output.push(']'); - } - _ => output.push_str(""), - }, - Some('A') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format u8 array - output.push('['); - for (i, byte) in b.iter().enumerate() { - if i > 0 { - output.push_str(", "); - } - output.push_str(&format!("{}", byte)); - } - output.push(']'); - } - _ => output.push_str(""), - }, - Some('%') => output.push('%'), - Some(_) | None => output.push_str("<%?>"), - } - } else { - output.push(c); - } - } - match &self.verbose_writer { - Some(writer) => { - let _ = writeln!(writer.borrow_mut(), "📜 Guest Log: {}", output); - } - None => { - println!("📜 Guest Log: {}", output); - } - } - 0 - } - - fn sys_call_program( - &mut self, - args: [u32; 6], - memory: Memory, - host: &mut Box, - metering: &mut dyn Metering, - ) -> u32 { - let to_ptr = args[0] as usize; - let from_ptr = args[1] as usize; - let input_ptr = args[2] as usize; - let input_len = args[3] as usize; - let result_ptr: u32; - let page_index: usize; - if matches!(metering.on_call(input_len), MeterResult::Halt) { - panic!("Metering halted SYSCALL_CALL_PROGRAM"); - } - { - let borrowed_memory = memory.as_ref(); - let to_slice = match borrowed_memory.mem_slice(to_ptr, to_ptr + 20) { - Some(r) => r, - None => return 0, - }; - let from_slice = match borrowed_memory.mem_slice(from_ptr, from_ptr + 20) { - Some(r) => r, - None => return 0, - }; - let input_slice = match borrowed_memory.mem_slice(input_ptr, input_ptr + input_len) { - Some(r) => r, - None => return 0, - }; - let mut to_bytes = [0u8; 20]; - let mut from_bytes = [0u8; 20]; - to_bytes.copy_from_slice(&to_slice); - from_bytes.copy_from_slice(&from_slice); - let input_vec = input_slice.to_vec(); - (result_ptr, page_index) = host.call_program(from_bytes, to_bytes, input_vec); - } - { - let borrowed_memory = memory.as_ref(); - let result_bytes = match host.read_memory_page(page_index, result_ptr, RESULT_SIZE) { - Some(b) => b, - None => return 0, - }; - if matches!(metering.on_alloc(result_bytes.len()), MeterResult::Halt) { - panic!("Metering halted alloc for call_program result"); - } - borrowed_memory.alloc_on_heap(&result_bytes) - } - } - - fn sys_alloc( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let size = args[0] as usize; // A0 register - let align = args[1] as usize; // A1 register - - if matches!(metering.on_alloc(size), MeterResult::Halt) { - panic!("Metering halted SYSCALL_ALLOC"); - } - - if size == 0 { - println!("VM Alloc: Invalid size 0"); - return 0; - } - - // Validate alignment (must be power of 2) - if align == 0 || (align & (align - 1)) != 0 { - println!("VM Alloc: Invalid alignment {}", align); - return 0; - } - - let current_heap = memory.next_heap(); - - // Initialize heap pointer if not set (no code has been written) - if current_heap == 0 { - memory.set_next_heap(HEAP_PTR_OFFSET); - } - - // Allocate aligned memory on heap - let data = vec![0u8; size]; - let ptr = memory.alloc_on_heap(&data); - - if ptr == 0 { - println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); - return 0; - } - - // Check if allocated address meets alignment requirements - if (ptr as usize) % align != 0 { - // Re-allocate with enough space for alignment - let total_size = size + align - 1; - let padded_data = vec![0u8; total_size]; - let padded_ptr = memory.alloc_on_heap(&padded_data); - if padded_ptr == 0 { - println!( - "VM Alloc: Out of memory, failed to allocate {} bytes for alignment", - total_size - ); - return 0; - } - // Return properly aligned pointer within the allocated region - let aligned_ptr = ((padded_ptr as usize + align - 1) & !(align - 1)) as u32; - return aligned_ptr; - } - - ptr - } - - fn sys_dealloc( - &mut self, - args: [u32; 6], - _memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let size = args[1] as usize; - if matches!(metering.on_alloc(size), MeterResult::Halt) { - panic!("Metering halted SYSCALL_DEALLOC"); - } - // Note: This VM uses a simple bump allocator, so we can't actually free memory - // In a real VM, you'd implement a proper allocator with free lists - // For now, this is a no-op since the memory will be reclaimed when the VM exits - 0 - } - - fn sys_transfer( - &mut self, - args: [u32; 6], - memory: Memory, - host: &mut Box, - metering: &mut dyn Metering, - ) -> u32 { - // args: a2=to ptr, a3=value_lo, a4=value_hi - let to_ptr = args[1] as usize; - let value_lo = args[2] as u64; - let value_hi = args[3] as u64; - let value = value_lo | (value_hi << 32); - - if matches!( - metering.on_syscall_data(SYSCALL_TRANSFER, 20), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_TRANSFER"); - } - - let borrowed = memory.as_ref(); - let to_slice = borrowed - .mem_slice(to_ptr, to_ptr + 20) - .expect("invalid to ptr"); - - let mut to = [0u8; 20]; - to.copy_from_slice(to_slice.as_ref()); - - if host.transfer(to, value) { - 0 - } else { - 1 - } - } - - fn sys_balance( - &mut self, - args: [u32; 6], - memory: Memory, - host: &mut Box, - metering: &mut dyn Metering, - ) -> u32 { - // args: a1 = address pointer (20 bytes) - let addr_ptr = args[0] as usize; - if matches!( - metering.on_syscall_data(SYSCALL_BALANCE, 20), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_BALANCE"); - } - let addr = { - let borrowed = memory.as_ref(); - let addr_slice = borrowed - .mem_slice(addr_ptr, addr_ptr + 20) - .expect("invalid addr ptr"); - let mut addr = [0u8; 20]; - addr.copy_from_slice(addr_slice.as_ref()); - addr - }; - - let bal = host.balance(addr); - memory.alloc_on_heap(&bal.to_le_bytes()) - } -} diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 1a3e889..e71125d 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -3,7 +3,7 @@ use crate::host_interface::HostInterface; use crate::memory::Memory; use crate::metering::Metering; use crate::registers::Register; -use crate::sys_call::{DefaultSyscallHandler, SyscallHandler}; +use crate::sys_call::SyscallHandler; use core::cell::RefCell; use std::rc::Rc; use storage::Storage; @@ -38,33 +38,14 @@ pub struct VM { } impl VM { - /// Creates a new virtual machine with the specified memory, storage, and host, using the default syscall handler. + /// Creates a new virtual machine with the specified memory, storage, host, and syscall handler. pub fn new( memory: Memory, storage: Rc>, host: Box, + syscall_handler: Box, ) -> Self { - Self::new_with_syscall_handler( - memory, - storage, - host, - Box::new(DefaultSyscallHandler::new()), - ) - } - - /// Creates a new virtual machine with a writer for logging output. - pub fn new_with_writer( - memory: Memory, - storage: Rc>, - host: Box, - writer: Option>>, - ) -> Self { - Self::new_with_syscall_handler( - memory, - storage, - host, - Box::new(DefaultSyscallHandler::with_writer(writer)), - ) + Self::new_with_syscall_handler(memory, storage, host, syscall_handler) } /// Creates a new virtual machine with a custom syscall handler. From 9929e3a2b71269a603bd9c840be778a9d8816482 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 19 Dec 2025 17:18:43 +0100 Subject: [PATCH 09/70] Move kernel into program and route storage by account --- Cargo.lock | 4 +- Makefile | 2 +- crates/examples/Cargo.toml | 1 - crates/examples/src/allocator_demo.rs | 9 +- crates/examples/src/call_program.rs | 7 +- crates/examples/src/dex.rs | 83 ++++--- crates/examples/src/ecdsa_verify.rs | 20 +- crates/examples/src/erc20.rs | 93 +++---- crates/examples/src/lib_import.rs | 31 +-- crates/examples/src/logging.rs | 62 ++--- crates/examples/src/multi_func.rs | 38 +-- crates/examples/src/native_transfer.rs | 5 +- crates/examples/src/simple.rs | 21 +- crates/examples/src/storage.rs | 40 ++- .../examples/tests/binary_comparison_test.rs | 67 +++-- crates/examples/tests/common/config.rs | 12 + crates/examples/tests/common/ecdsa.rs | 32 +-- crates/examples/tests/common/router.rs | 23 ++ crates/examples/tests/common/state.rs | 2 +- crates/examples/tests/common/test_runner.rs | 45 +++- crates/examples/tests/common/utils.rs | 54 ++-- crates/examples/tests/ecdsa_payload_test.rs | 2 +- crates/examples/tests/examples_test.rs | 234 ++++++++---------- crates/os/Cargo.toml | 9 +- crates/os/src/allocator.rs | 41 --- crates/os/src/bin/bootloader_runner.rs | 2 +- crates/os/src/bootloader.rs | 29 +-- crates/os/src/lib.rs | 13 +- crates/os/src/memory/memory_page.rs | 8 +- crates/os/src/syscalls.rs | 152 ++++++++---- crates/os/tests/allocator_test.rs | 23 +- crates/program/Cargo.toml | 6 + crates/{os => program}/src/kernel.rs | 41 +-- crates/program/src/storage.rs | 36 +-- crates/program/src/storage_map.rs | 57 +++-- crates/types/src/address.rs | 4 +- crates/types/src/lib.rs | 2 +- crates/vm/Cargo.toml | 1 - crates/vm/src/cpu.rs | 29 +-- crates/vm/src/exe.rs | 6 - crates/vm/src/sys_call.rs | 4 - crates/vm/src/vm.rs | 27 +- 42 files changed, 716 insertions(+), 661 deletions(-) create mode 100644 crates/examples/tests/common/config.rs create mode 100644 crates/examples/tests/common/router.rs delete mode 100644 crates/os/src/allocator.rs rename crates/{os => program}/src/kernel.rs (62%) diff --git a/Cargo.lock b/Cargo.lock index c988648..293ffe8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,7 +145,6 @@ dependencies = [ name = "examples" version = "0.1.0" dependencies = [ - "avm", "compiler", "k256", "once_cell", @@ -264,7 +263,7 @@ dependencies = [ "compiler", "goblin", "program", - "storage", + "state", "types", "vm", ] @@ -487,7 +486,6 @@ version = "0.1.0" dependencies = [ "compiler", "state", - "storage", "types", ] diff --git a/Makefile b/Makefile index a6aa9ef..2f6683c 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ run_examples: RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples @echo "=== Building kernel ELF ===" @mkdir -p crates/os/bin - @$(AVM32) all --bin kernel --manifest-path crates/os/Cargo.toml --features guest_kernel --out-dir crates/os/bin + @$(AVM32) all --bin kernel --manifest-path crates/program/Cargo.toml --features guest_kernel --out-dir crates/os/bin @echo "=== Running example crate tests ===" cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../os/bin/kernel.elf" cargo test test_examples -- --nocapture @echo "=== Example programs build and tests complete ===" diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index e430fcb..66f2e25 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -11,7 +11,6 @@ k256 = { version = "0.13", default-features = false, features = ["arithmetic", " [dev-dependencies] types = { path = "../types" } # adjust path as needed compiler = { path = "../compiler" } # adjust path as needed -avm = { path = "../avm" } # adjust path as needed state = { path = "../state" } os = { path = "../os" } once_cell = "1.19.0" diff --git a/crates/examples/src/allocator_demo.rs b/crates/examples/src/allocator_demo.rs index 508f275..9a2ba28 100644 --- a/crates/examples/src/allocator_demo.rs +++ b/crates/examples/src/allocator_demo.rs @@ -3,14 +3,17 @@ extern crate alloc; -use program::{entrypoint, types::result::Result, types::address::Address, require, vm_panic, DataParser}; +use program::{ + DataParser, entrypoint, require, types::address::Address, types::result::Result, vm_panic, +}; /// Guest program that demonstrates heap allocation using VM syscalls entrypoint!(main); -fn main(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn main(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // Need to import alloc types after entrypoint macro includes the allocator - use alloc::vec::Vec; use alloc::collections::BTreeMap; + use alloc::vec::Vec; // Expect at least 6 u32 values in little-endian form: // - first 3 populate the Vec diff --git a/crates/examples/src/call_program.rs b/crates/examples/src/call_program.rs index f40450a..7b08dd0 100644 --- a/crates/examples/src/call_program.rs +++ b/crates/examples/src/call_program.rs @@ -3,9 +3,9 @@ extern crate program; -use program::{entrypoint, types::result::Result, require, vm_panic, DataParser}; use program::call::call; use program::types::address::Address; +use program::{DataParser, entrypoint, require, types::result::Result, vm_panic}; // Include the auto-generated ABI client code for simple program include!("../bin/simple_abi.rs"); @@ -16,7 +16,8 @@ include!("../bin/simple_abi.rs"); /// The program expects: /// - 20 bytes: Address of the simple contract /// - 8 bytes: Two u32 values to compare (4 bytes each) -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, caller: Address, data: &[u8]) -> Result { + let _ = program; // Ensure there's enough data require(data.len() == 28, b"input data must be 28 bytes"); @@ -37,7 +38,7 @@ fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result call_data[4..8].copy_from_slice(&second.to_le_bytes()); // Call the simple contract using the generated client's call_main method - let ret = match simple_client.call_main(&_caller, &call_data) { + let ret = match simple_client.call_main(&caller, &call_data) { Some(result) => result, None => vm_panic(b"program call failed"), }; diff --git a/crates/examples/src/dex.rs b/crates/examples/src/dex.rs index 93c8d1d..27dfa85 100644 --- a/crates/examples/src/dex.rs +++ b/crates/examples/src/dex.rs @@ -4,11 +4,11 @@ extern crate program; use program::{ + DataParser, Map, call::call, - entrypoint, event, fire_event, persist_struct, DataParser, require, vm_panic, transfer, - hex_address, + entrypoint, event, fire_event, hex_address, persist_struct, require, transfer, types::{address::Address, o::O, result::Result}, - Map, + vm_panic, }; // Generated ABI client for ERC20 (included like in call_program example) @@ -52,8 +52,8 @@ const ADD_LIQUIDITY: u8 = 0x01; const REMOVE_LIQUIDITY: u8 = 0x02; const SWAP: u8 = 0x03; -fn load_pool() -> Pool { - match Pool::load() { +fn load_pool(program: &Address) -> Pool { + match Pool::load(program) { O::Some(p) => p, O::None => Pool { reserve_am: 0, @@ -63,14 +63,14 @@ fn load_pool() -> Pool { } } -fn get_liquidity(owner: Address) -> u128 { - match Liquidity::get(owner) { +fn get_liquidity(program: &Address, owner: Address) -> u128 { + match Liquidity::get(program, owner) { O::Some(v) => v, O::None => 0, } } -fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { +fn add_liquidity(program: Address, caller: Address, mut parser: DataParser) -> Result { // Adds liquidity by pulling both legs (native AM and ERC20) from the caller, // mints LP shares proportional to the existing reserves, and emits an event. let erc20 = Erc20Contract::new(erc20_address()); @@ -82,16 +82,16 @@ fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> require(token_in <= u32::MAX as u128, b"add: token overflow"); // Collect AM from caller into the pool address (native balance increases). - require(transfer!(&self_addr, am_in), b"add: am transfer failed"); + require(transfer!(&program, am_in), b"add: am transfer failed"); // Pull ERC20 from caller into the pool address. let ok = erc20 - .transfer(&caller, self_addr, token_in as u32) + .transfer(&caller, program, token_in as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"add: token transfer failed"); - let mut pool = load_pool(); + let mut pool = load_pool(&program); let minted = if pool.total_liquidity == 0 { am_in as u128 @@ -107,10 +107,10 @@ fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> pool.reserve_am = pool.reserve_am.saturating_add(am_in as u128); pool.reserve_token = pool.reserve_token.saturating_add(token_in); pool.total_liquidity = pool.total_liquidity.saturating_add(minted); - pool.store(); + pool.store(&program); - let user_liq = get_liquidity(caller).saturating_add(minted); - Liquidity::set(caller, user_liq); + let user_liq = get_liquidity(&program, caller).saturating_add(minted); + Liquidity::set(&program, caller, user_liq); if token_in <= u64::MAX as u128 { fire_event!(LiquidityAdded::new(caller, am_in, token_in as u64)); @@ -121,15 +121,15 @@ fn add_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> res } -fn remove_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { +fn remove_liquidity(program: Address, caller: Address, mut parser: DataParser) -> Result { // Burns LP shares for AM + ERC20 payouts, updates reserves, and emits LiquidityRemoved. let erc20 = Erc20Contract::new(erc20_address()); require(parser.remaining() >= 8, b"remove: missing args"); let shares = parser.read_u64() as u128; require(shares > 0, b"remove: zero shares"); - let mut pool = load_pool(); - let user_shares = get_liquidity(caller); + let mut pool = load_pool(&program); + let user_shares = get_liquidity(&program, caller); require(user_shares >= shares, b"remove: not enough shares"); require(pool.total_liquidity > 0, b"remove: empty pool"); @@ -141,23 +141,30 @@ fn remove_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) pool.reserve_am = pool.reserve_am.saturating_sub(am_out); pool.reserve_token = pool.reserve_token.saturating_sub(token_out); pool.total_liquidity = pool.total_liquidity.saturating_sub(shares); - pool.store(); + pool.store(&program); - Liquidity::set(caller, user_shares - shares); + Liquidity::set(&program, caller, user_shares - shares); // Pay out ERC20 tokens from pool balance. require(token_out <= u32::MAX as u128, b"remove: token overflow"); let ok = erc20 - .transfer(&self_addr, caller, token_out as u32) + .transfer(&program, caller, token_out as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"remove: token transfer failed"); // Pay native AM out to the provider. Note: with the current host interface, // native transfers debit the caller context. - require(transfer!(&caller, am_out as u64), b"remove: am transfer failed"); + require( + transfer!(&caller, am_out as u64), + b"remove: am transfer failed", + ); - fire_event!(LiquidityRemoved::new(caller, am_out as u64, token_out as u64)); + fire_event!(LiquidityRemoved::new( + caller, + am_out as u64, + token_out as u64 + )); // AM payouts are reported in the result for visibility. let mut res = Result::new(true, 0); @@ -168,7 +175,7 @@ fn remove_liquidity(self_addr: Address, caller: Address, mut parser: DataParser) res } -fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { +fn swap(program: Address, caller: Address, mut parser: DataParser) -> Result { // Constant-product swap. Direction 0 = AM -> ERC20, Direction 1 = ERC20 -> AM. let erc20 = Erc20Contract::new(erc20_address()); require(parser.remaining() >= 9, b"swap: missing args"); @@ -176,26 +183,32 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { let amount = parser.read_u64(); require(amount > 0, b"swap: zero amount"); - let mut pool = load_pool(); - require(pool.reserve_am > 0 && pool.reserve_token > 0, b"swap: empty pool"); + let mut pool = load_pool(&program); + require( + pool.reserve_am > 0 && pool.reserve_token > 0, + b"swap: empty pool", + ); if direction == 0 { let am_in = amount; // Collect AM into the pool. - let ok = transfer!(&self_addr, am_in); + let ok = transfer!(&program, am_in); require(ok, b"swap: am transfer failed"); let token_out = (am_in as u128 * pool.reserve_token) / (pool.reserve_am + am_in as u128); require(token_out > 0, b"swap: zero output"); - require(token_out <= pool.reserve_token, b"swap: insufficient tokens"); + require( + token_out <= pool.reserve_token, + b"swap: insufficient tokens", + ); require(token_out <= u32::MAX as u128, b"swap: token overflow"); pool.reserve_am = pool.reserve_am.saturating_add(am_in as u128); pool.reserve_token = pool.reserve_token.saturating_sub(token_out); - pool.store(); + pool.store(&program); let ok = erc20 - .transfer(&self_addr, caller, token_out as u32) + .transfer(&program, caller, token_out as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"swap: token transfer failed"); @@ -212,7 +225,7 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { // Pull ERC20 into the pool. let ok = erc20 - .transfer(&caller, self_addr, token_in as u32) + .transfer(&caller, program, token_in as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"swap: token transfer failed"); @@ -224,7 +237,7 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { pool.reserve_token = pool.reserve_token.saturating_add(token_in); pool.reserve_am = pool.reserve_am.saturating_sub(am_out); - pool.store(); + pool.store(&program); // Pay native AM to the trader and leave ERC20 in the pool. require(transfer!(&caller, am_out as u64), b"swap: am payout failed"); @@ -237,7 +250,7 @@ fn swap(self_addr: Address, caller: Address, mut parser: DataParser) -> Result { } } -fn dex_entry(self_addr: Address, caller: Address, data: &[u8]) -> Result { +fn dex_entry(program: Address, caller: Address, data: &[u8]) -> Result { // Simple selector-based router: first byte is op, remainder is args for the op handlers. if data.is_empty() { vm_panic(b"missing selector"); @@ -247,9 +260,9 @@ fn dex_entry(self_addr: Address, caller: Address, data: &[u8]) -> Result { let op = parser.read_bytes(1)[0]; match op { - ADD_LIQUIDITY => add_liquidity(self_addr, caller, parser), - REMOVE_LIQUIDITY => remove_liquidity(self_addr, caller, parser), - SWAP => swap(self_addr, caller, parser), + ADD_LIQUIDITY => add_liquidity(program, caller, parser), + REMOVE_LIQUIDITY => remove_liquidity(program, caller, parser), + SWAP => swap(program, caller, parser), _ => vm_panic(b"unknown selector"), } } diff --git a/crates/examples/src/ecdsa_verify.rs b/crates/examples/src/ecdsa_verify.rs index f5632a2..47942b1 100644 --- a/crates/examples/src/ecdsa_verify.rs +++ b/crates/examples/src/ecdsa_verify.rs @@ -2,10 +2,10 @@ #![no_main] extern crate program; -use k256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; +use k256::ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier}; use program::{ - entrypoint, log, logf, require, types::address::Address, types::result::Result, vm_panic, - DataParser, HexCodec, + DataParser, HexCodec, entrypoint, log, logf, require, types::address::Address, + types::result::Result, vm_panic, }; /// ECDSA verification example using k256. @@ -14,17 +14,23 @@ use program::{ /// - N bytes: SEC1-encoded pubkey /// - 64 bytes: signature (r||s) /// - 32 bytes: message hash (already hashed) -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; let mut parser = DataParser::new(data); let pk_len = parser.read_bytes(1)[0] as usize; - require(pk_len == 33 || pk_len == 65, b"pubkey must be 33 or 65 bytes"); + require( + pk_len == 33 || pk_len == 65, + b"pubkey must be 33 or 65 bytes", + ); let pk_bytes = parser.read_bytes(pk_len); let sig_bytes = parser.read_bytes(64); let hash = parser.read_bytes(32); - let verifying_key = VerifyingKey::from_sec1_bytes(pk_bytes).unwrap_or_else(|_| vm_panic(b"invalid pubkey")); - let signature = Signature::from_slice(sig_bytes).unwrap_or_else(|_| vm_panic(b"invalid signature")); + let verifying_key = + VerifyingKey::from_sec1_bytes(pk_bytes).unwrap_or_else(|_| vm_panic(b"invalid pubkey")); + let signature = + Signature::from_slice(sig_bytes).unwrap_or_else(|_| vm_panic(b"invalid signature")); // Log the received inputs in hex for visibility logf!("ecdsa_verify: pk_len=%d", pk_len as u32); diff --git a/crates/examples/src/erc20.rs b/crates/examples/src/erc20.rs index 2d37136..fe7c58c 100644 --- a/crates/examples/src/erc20.rs +++ b/crates/examples/src/erc20.rs @@ -2,11 +2,12 @@ #![no_main] extern crate program; -use program::{entrypoint, event, - fire_event, log, logf, persist_struct, - require, router::route, DataParser, - types::{address::Address, o::O, result::Result}, vm_panic, Map}; - +use program::{ + DataParser, Map, entrypoint, event, fire_event, log, logf, persist_struct, require, + router::route, + types::{address::Address, o::O, result::Result}, + vm_panic, +}; // Persistent structs persist_struct!(Metadata { @@ -27,37 +28,41 @@ event!(Transfer { Map!(Balances); -unsafe fn main_entry(program: Address, caller: Address, data: &[u8]) -> Result { - route(data, program, caller, - |to, from, call| match call.selector { - 0x01 => { - init(caller, call.args); - Result::new(true, 0) - }, - 0x02 => { - let mut parser = DataParser::new(call.args); - let to = parser.read_address(); - let amount = parser.read_u32(); - transfer(caller, to, amount); - Result::new(true, 0) - }, - 0x05 => { - let mut parser = DataParser::new(call.args); - let owner = parser.read_address(); - let b = balance_of(owner); - Result::with_u32(b) - }, - _ => vm_panic(b"unknown selector"), +unsafe fn main_entry(program: Address, caller: Address, data: &[u8]) -> Result { + route(data, program, caller, |to, from, call| { + match call.selector { + 0x01 => { + init(&program, caller, call.args); + Result::new(true, 0) + } + 0x02 => { + let mut parser = DataParser::new(call.args); + let to = parser.read_address(); + let amount = parser.read_u32(); + transfer(&program, caller, to, amount); + Result::new(true, 0) + } + 0x05 => { + let mut parser = DataParser::new(call.args); + let owner = parser.read_address(); + let b = balance_of(&program, owner); + Result::with_u32(b) + } + _ => vm_panic(b"unknown selector"), + } }) } -fn init(caller: Address, args: &[u8]) { +fn init(program: &Address, caller: Address, args: &[u8]) { logf!("init called"); - let mut meta = match Metadata::load() { + let mut meta = match Metadata::load(program) { O::Some(m) => vm_panic(b"already initialized"), - O::None => Metadata { total_supply: 0, decimals: 0 }, + O::None => Metadata { + total_supply: 0, + decimals: 0, + }, }; - + logf!("initializing"); let mut parser = DataParser::new(args); @@ -69,41 +74,41 @@ fn init(caller: Address, args: &[u8]) { meta.total_supply = total_supply; meta.decimals = decimals; - meta.store(); + meta.store(program); // mint to caller - mint(caller, total_supply); + mint(program, caller, total_supply); } -fn mint(caller: Address, val: u32) { +fn mint(program: &Address, caller: Address, val: u32) { logf!("minting: %d tokens", val); fire_event!(Minted::new(caller, val)); - Balances::set(caller, val); + Balances::set(program, caller, val); } -fn transfer(caller: Address, to: Address, amount: u32) { - let from_bal = match Balances::get(caller) { +fn transfer(program: &Address, caller: Address, to: Address, amount: u32) { + let from_bal = match Balances::get(program, caller) { O::Some(bal) => bal, O::None => 0, }; - + if from_bal < amount { vm_panic(b"insufficient"); } - let to_bal = match Balances::get(to) { + let to_bal = match Balances::get(program, to) { O::Some(bal) => bal, O::None => 0, }; - - Balances::set(caller, from_bal - amount); - Balances::set(to, to_bal + amount); - + + Balances::set(program, caller, from_bal - amount); + Balances::set(program, to, to_bal + amount); + fire_event!(Transfer::new(caller, to, amount)); } -fn balance_of(owner: Address) -> u32 { - match Balances::get(owner) { +fn balance_of(program: &Address, owner: Address) -> u32 { + match Balances::get(program, owner) { O::Some(bal) => bal, O::None => 0, } diff --git a/crates/examples/src/lib_import.rs b/crates/examples/src/lib_import.rs index 8ad3727..c75c654 100644 --- a/crates/examples/src/lib_import.rs +++ b/crates/examples/src/lib_import.rs @@ -2,57 +2,58 @@ #![no_main] extern crate program; -use program::{entrypoint, types::result::Result, require}; use program::types::address::Address; +use program::{entrypoint, require, types::result::Result}; // Import the sha2 library for hashing -use sha2::{Sha256, Digest}; +use sha2::{Digest, Sha256}; /// Example program that imports and uses an external library (sha2) -/// +/// /// This demonstrates importing and using an external cryptographic library /// within a smart contract environment. -/// +/// /// CONTRACT BEHAVIOR: /// - Takes arbitrary input data /// - Computes SHA-256 hash of the input /// - Returns the 32-byte hash -/// +/// /// INPUT FORMAT: Any arbitrary bytes -/// +/// /// OUTPUT FORMAT: Returns a Result struct with: /// - success: true (always succeeds if input is valid) /// - error_code: 0 (no error) /// - data_len: 32 (size of SHA-256 hash) /// - data: The SHA-256 hash as 32 bytes -/// +/// /// REAL-WORLD USAGE: /// - Data integrity verification /// - Creating commitments for reveal schemes /// - Generating deterministic IDs from data /// - Proof of data existence at a point in time -fn hasher_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn hasher_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // Validate that we have some input data require(data.len() > 0, b"Input data cannot be empty"); - + // Create a new SHA-256 hasher instance let mut hasher = Sha256::new(); - + // Feed the input data to the hasher hasher.update(data); - + // Compute the hash and get the result as a fixed array let hash_result = hasher.finalize(); - + // Create a result with the hash data let mut result = Result::new(true, 0); - + // Copy the 32-byte hash into the result's data field result.data[..32].copy_from_slice(&hash_result[..]); result.data_len = 32; - + result } // Register the function as the entrypoint -entrypoint!(hasher_entry); \ No newline at end of file +entrypoint!(hasher_entry); diff --git a/crates/examples/src/logging.rs b/crates/examples/src/logging.rs index 57f6996..f47065b 100644 --- a/crates/examples/src/logging.rs +++ b/crates/examples/src/logging.rs @@ -2,62 +2,62 @@ #![no_main] extern crate program; -use program::{entrypoint, types::result::Result, logf, log, concat_str, DataParser}; -use program::types::address::Address; use core::fmt; +use program::types::address::Address; +use program::{DataParser, concat_str, entrypoint, log, logf, types::result::Result}; /// Comprehensive logging demonstration showing all format specifiers -unsafe fn logging(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +unsafe fn logging(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // Simple string logging logf!("=== Logging Demo Started ==="); - + // Integer formats let num = 42; logf!("Decimal: %d", num); logf!("Unsigned: %u", num); logf!("Hexadecimal: %x", 0xDEADBEEF); - + // Multiple values in one log let x = 10; let y = 20; logf!("x=%d, y=%d, sum=%d", x, y, x + y); - + // Character logging let ch = 'A' as u32; logf!("Character: %c", ch); - + // Floating point let pi_bits = 3.14159f32.to_bits(); logf!("Pi approximation: %f", pi_bits); - + // String logging - now simplified! let msg = b"Hello, VM!"; log!("Message: %s", msg); - + // String concatenation - requires a buffer in no_std (even though we have an allocator) - let mut buffer = [0u8; 64]; // Stack-allocated storage + let mut buffer = [0u8; 64]; // Stack-allocated storage let greeting = concat_str!(buffer, b"Hello, ", b"World", b"!"); log!("Concatenated: %s", greeting); - - + // Byte array logging (hex format) - simplified! let bytes = [0xDE, 0xAD, 0xBE, 0xEF]; log!("Bytes (hex): %b", bytes); - + // Array of u32s - simplified! let numbers = [1u32, 2, 3, 4, 5]; log!("Numbers: %a", numbers); - + // Array of u8s (decimal format) - simplified! let bytes_decimal = [10u8, 20, 30, 40, 50]; log!("Bytes (decimal): %A", bytes_decimal); - + // Process input data let mut parser = DataParser::new(data); if parser.remaining() >= 4 { let value = parser.read_u32(); logf!("Input value: %d (0x%x)", value, value); - + // Log remaining bytes if any if parser.remaining() > 0 { let remaining = parser.read_bytes(parser.remaining()); @@ -67,52 +67,56 @@ unsafe fn logging(_self_address: Address, _caller: Address, data: &[u8]) -> Resu } else { logf!("Input too short: %d bytes", data.len() as u32); } - + // Demonstrate escape sequence logf!("100%% complete!"); - + // Complex example with mixed types let score = 95; let grade = 'A' as u32; let bonus = 5; - logf!("Score: %d + Bonus: %d = Total: %d", - score, bonus, score + bonus); + logf!( + "Score: %d + Bonus: %d = Total: %d", + score, + bonus, + score + bonus + ); logf!("Grade: %c", grade); - + // Large array (partial display for efficiency) - simplified! let large_array = [1u32, 2, 3, 4, 5, 6, 7, 8, 9, 10]; log!("Large array: %a", large_array); - + // Debug and Display trait demonstrations logf!("=== Debug and Display Trait Logging ==="); - + // Create a custom struct that implements Debug and Display let point = Point { x: 10, y: 20 }; logf!("Point (Debug): %s", debug: point); logf!("Point (Display): %s", display: point); - + // Test with Option types (implements Debug) let some_value: Option = Some(42); let none_value: Option = None; logf!("Some value: %s", debug: some_value); logf!("None value: %s", debug: none_value); - + // Test with Result types (implements Debug) let ok_result: core::result::Result = Ok(100); let err_result: core::result::Result = Err("error message"); logf!("Ok result: %s", debug: ok_result); logf!("Err result: %s", debug: err_result); - + // Test with arrays (Debug) let debug_array = [1, 2, 3, 4, 5]; logf!("Array debug: %s", debug: debug_array); - + // Test with tuples (Debug) let tuple = (42, "hello", true); logf!("Tuple: %s", debug: tuple); - + logf!("=== Logging Demo Complete ==="); - + Result::new(true, 0) } diff --git a/crates/examples/src/multi_func.rs b/crates/examples/src/multi_func.rs index 3f214c1..02cc61d 100644 --- a/crates/examples/src/multi_func.rs +++ b/crates/examples/src/multi_func.rs @@ -3,50 +3,52 @@ extern crate program; -use program::{entrypoint, types::result::Result, vm_panic, require, DataParser}; +use program::router::route; use program::types::address::Address; -use program::router::{route}; +use program::{DataParser, entrypoint, require, types::result::Result, vm_panic}; /// Main entry point for the smart contract. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates a multi-function smart contract /// that can handle different operations based on a selector. This is a common /// pattern in smart contract development, similar to how web APIs work. -/// +/// /// FUNCTION ROUTING: The contract uses a selector (function ID) to determine /// which function to call. This allows one contract to provide multiple /// different operations. -/// +/// /// PARAMETERS: -/// - _self_address: The address of this contract (unused in this example) +/// - program: The address of this contract (unused in this example) /// - _caller: The address calling this contract (unused in this example) /// - data: Binary data containing the function selector and arguments -/// +/// /// RETURN VALUE: A Result indicating success/failure and any return data -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, data: &[u8]) -> Result { // EDUCATIONAL: Use the router to handle multiple function calls // The router decodes the input data and calls the appropriate function - route(data, _self_address, _caller, |to, from,call| match call.selector { - 0x01 => compare(call.args), // Function selector 0x01 = compare function - 0x02 => other(call.args), // Function selector 0x02 = other function - _ => vm_panic(b"unknown selector"), // Unknown selector = panic + route(data, program, _caller, |to, from, call| { + match call.selector { + 0x01 => compare(call.args), // Function selector 0x01 = compare function + 0x02 => other(call.args), // Function selector 0x02 = other function + _ => vm_panic(b"unknown selector"), // Unknown selector = panic + } }) } /// Compares two 32-bit integers and returns the larger one. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates how to handle binary data in smart /// contracts. The function receives raw bytes and must parse them into /// meaningful data structures. -/// +/// /// INPUT FORMAT: 8 bytes total /// - First 4 bytes: First integer (little-endian) /// - Last 4 bytes: Second integer (little-endian) -/// +/// /// RETURN LOGIC: /// - If first number > second number: success = true, error_code = first number /// - If first number <= second number: success = false, error_code = second number -/// +/// /// EDUCATIONAL NOTE: The return value uses the Result struct's fields in a /// non-standard way - error_code actually contains the larger number. This /// is just for demonstration purposes. @@ -71,11 +73,11 @@ fn compare(data: &[u8]) -> Result { } /// Example function that always fails. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates error handling in smart contracts. /// Sometimes functions need to fail intentionally (e.g., when conditions /// aren't met or for testing purposes). -/// +/// /// USAGE: This function is called when selector 0x02 is used. It always /// panics with the message "Intentional failure", which will cause the /// entire transaction to fail and revert any state changes. diff --git a/crates/examples/src/native_transfer.rs b/crates/examples/src/native_transfer.rs index 66a790b..844b589 100644 --- a/crates/examples/src/native_transfer.rs +++ b/crates/examples/src/native_transfer.rs @@ -3,15 +3,16 @@ extern crate program; -use program::{entrypoint, require, DataParser}; use program::types::address::Address; use program::types::result::Result; +use program::{DataParser, entrypoint, require}; /// Demonstrates transferring the native AM token from the caller to a target /// address using the VM's transfer syscall. The input payload is: /// - 20 bytes: destination address /// - 8 bytes: amount (little-endian u64) -fn transfer_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn transfer_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; let mut parser = DataParser::new(data); // Need at least 20 bytes for the address and 8 bytes for the value require(parser.remaining() >= 28, b"transfer: need addr + amount"); diff --git a/crates/examples/src/simple.rs b/crates/examples/src/simple.rs index 76e4e20..9a709b3 100644 --- a/crates/examples/src/simple.rs +++ b/crates/examples/src/simple.rs @@ -2,42 +2,43 @@ #![no_main] extern crate program; -use program::{entrypoint, types::result::Result, require, DataParser}; -use program::types::address::Address; +use program::types::address::Address; +use program::{DataParser, entrypoint, require, types::result::Result}; /// Simple smart contract that compares two 32-bit integers. -/// +/// /// EDUCATIONAL PURPOSE: This demonstrates a basic smart contract that: /// - Accepts input data (two 32-bit integers) /// - Performs a simple comparison operation /// - Returns a result with the larger number stored in data -/// +/// /// CONTRACT BEHAVIOR: /// - Takes 8 bytes of input data (two 32-bit integers) /// - Compares the first integer with the second /// - Returns success=true if first > second, success=false otherwise /// - Stores the larger value in the data field -/// +/// /// INPUT FORMAT: The contract expects exactly 8 bytes: /// - Bytes 0-3: First 32-bit integer (little-endian) /// - Bytes 4-7: Second 32-bit integer (little-endian) -/// +/// /// OUTPUT FORMAT: Returns a Result struct with: /// - success: true if first > second, false otherwise /// - error_code: 0 (no error) /// - data_len: 4 (size of u32) /// - data: The larger of the two input values stored as 4 bytes -/// +/// /// REAL-WORLD USAGE: This type of contract could be used for: /// - Simple validation logic /// - Conditional execution based on input values /// - Basic decision-making in decentralized applications -/// +/// /// SECURITY CONSIDERATIONS: /// - Input validation prevents buffer overflows /// - No external calls or state modifications /// - Deterministic execution for all inputs -fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, data: &[u8]) -> Result { + let _ = program; // EDUCATIONAL: Validate input data length to prevent buffer overflows // This is a critical security practice in smart contracts require(data.len() >= 8, b"Input data must be at least 8 bytes long"); @@ -50,7 +51,7 @@ fn my_vm_entry(_self_address: Address, _caller: Address, data: &[u8]) -> Result // EDUCATIONAL: Perform the comparison and return appropriate result // This demonstrates conditional logic in smart contracts - if first > second { + if first > second { // Return success with the larger number (first) stored in data return Result::with_u32(first); } else { diff --git a/crates/examples/src/storage.rs b/crates/examples/src/storage.rs index 4b3187f..a4a5def 100644 --- a/crates/examples/src/storage.rs +++ b/crates/examples/src/storage.rs @@ -2,9 +2,9 @@ #![no_main] extern crate program; -use program::{entrypoint, types::result::Result, require}; -use program::types::address::Address; use program::persist_struct; +use program::types::address::Address; +use program::{entrypoint, require, types::result::Result}; // Struct 1: User profile persist_struct!(User { @@ -19,13 +19,20 @@ persist_struct!(Config { timeout_ms: u64, }); -fn my_vm_entry(_self_address: Address, _caller: Address, _data: &[u8]) -> Result { +fn my_vm_entry(program: Address, _caller: Address, _data: &[u8]) -> Result { // --- User --- - require(User::load().is_none() == true, b"user already exists"); - let mut user = User{id: 1000, active: false, level: 3}; + require( + User::load(&program).is_none() == true, + b"user already exists", + ); + let mut user = User { + id: 1000, + active: false, + level: 3, + }; user.level = 4; user.id = 40000; - user.store(); + user.store(&program); // ... change local copy ... user.level = 5; @@ -33,16 +40,22 @@ fn my_vm_entry(_self_address: Address, _caller: Address, _data: &[u8]) -> Result // ... later ... - let reloaded_user = User::load().expect("user not found"); + let reloaded_user = User::load(&program).expect("user not found"); require(reloaded_user.level == 4, b"user level must be 4"); require(reloaded_user.id == 40000, b"user id must be 40000"); // --- Config --- - require(Config::load().is_none() == true, b"config already exists"); - let mut config = Config{retries: 10, timeout_ms: 10}; + require( + Config::load(&program).is_none() == true, + b"config already exists", + ); + let mut config = Config { + retries: 10, + timeout_ms: 10, + }; config.retries = 13; config.timeout_ms = 100000; - config.store(); + config.store(&program); // ... change local copy ... config.retries = 15; @@ -50,9 +63,12 @@ fn my_vm_entry(_self_address: Address, _caller: Address, _data: &[u8]) -> Result // ... later ... - let reloaded_config = Config::load().expect("config not found"); + let reloaded_config = Config::load(&program).expect("config not found"); require(reloaded_config.retries == 13, b"config retries must be 13"); - require(reloaded_config.timeout_ms == 100000, b"config timeout_ms must be 100000"); + require( + reloaded_config.timeout_ms == 100000, + b"config timeout_ms must be 100000", + ); Result::new(true, 0) } diff --git a/crates/examples/tests/binary_comparison_test.rs b/crates/examples/tests/binary_comparison_test.rs index 66c482c..2525ad5 100644 --- a/crates/examples/tests/binary_comparison_test.rs +++ b/crates/examples/tests/binary_comparison_test.rs @@ -1,19 +1,22 @@ +use core::cell::RefCell; +use core::fmt::Write; use std::fs; use std::path::Path; use std::rc::Rc; -use core::cell::RefCell; -use core::fmt::Write; // Import the test runner and related modules #[path = "examples_test.rs"] mod examples_test; +#[path = "common/config.rs"] +mod config; + #[path = "common/utils.rs"] mod utils; use examples_test::TestRunner; -use k256::ecdsa::{signature::hazmat::PrehashVerifier, VerifyingKey, Signature}; use examples_test::build_ecdsa_payload; +use k256::ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier}; #[test] fn test_vm_binary_comparison() -> Result<(), String> { @@ -21,11 +24,14 @@ fn test_vm_binary_comparison() -> Result<(), String> { // Step 1: Create TestRunner with file output let vm_log_path = "/tmp/vm_binary_comparison.log"; - println!("Step 1: Running TestRunner with file output to: {}", vm_log_path); + println!( + "Step 1: Running TestRunner with file output to: {}", + vm_log_path + ); // Create a file writer for the TestRunner - let file = fs::File::create(vm_log_path) - .map_err(|e| format!("Failed to create log file: {}", e))?; + let file = + fs::File::create(vm_log_path).map_err(|e| format!("Failed to create log file: {}", e))?; // Create a Write adapter for the file struct FileWriter(fs::File); @@ -33,7 +39,9 @@ fn test_vm_binary_comparison() -> Result<(), String> { impl Write for FileWriter { fn write_str(&mut self, s: &str) -> core::fmt::Result { use std::io::Write; - self.0.write_all(s.as_bytes()).map_err(|_| core::fmt::Error)?; + self.0 + .write_all(s.as_bytes()) + .map_err(|_| core::fmt::Error)?; self.0.flush().map_err(|_| core::fmt::Error)?; Ok(()) } @@ -43,9 +51,9 @@ fn test_vm_binary_comparison() -> Result<(), String> { // Create TestRunner with file output and verbose mode for instruction tracing let runner = TestRunner::with_writer(writer) - .with_verbose(true) // Enable verbose mode for PC traces - .with_memory_size(512 * 1024) // Larger memory for crypto-heavy binaries - .with_max_pages(128); + .with_verbose(true) // Enable verbose mode for PC traces + .with_memory_size(512 * 1024) // Larger memory for crypto-heavy binaries + .with_max_pages(128); // Run all test cases runner.execute()?; @@ -60,16 +68,22 @@ fn test_vm_binary_comparison() -> Result<(), String> { println!("Step 2: VM log file created, size: {} bytes", log_size); // Step 3: Parse the log to extract test cases and instructions - let log_content = fs::read_to_string(vm_log_path) - .map_err(|e| format!("Failed to read log file: {}", e))?; + let log_content = + fs::read_to_string(vm_log_path).map_err(|e| format!("Failed to read log file: {}", e))?; let test_cases = extract_test_cases(&log_content); - println!("\nStep 3: Extracted {} test cases from log", test_cases.len()); + println!( + "\nStep 3: Extracted {} test cases from log", + test_cases.len() + ); // Step 4: Check for corresponding ELF binaries let binaries_dir = Path::new("../../target/avm32/release"); - println!("\nStep 4: Checking for ELF binaries in: {}", binaries_dir.display()); + println!( + "\nStep 4: Checking for ELF binaries in: {}", + binaries_dir.display() + ); let mut comparison_results = Vec::new(); @@ -123,7 +137,10 @@ fn test_vm_binary_comparison() -> Result<(), String> { for result in &comparison_results { println!("\n📊 {} ({})", result.test_name, result.binary_name); println!(" VM Instructions: {}", result.vm_instructions); - println!(" ELF Found: {}", if result.elf_found { "Yes" } else { "No" }); + println!( + " ELF Found: {}", + if result.elf_found { "Yes" } else { "No" } + ); if result.elf_found { println!(" Match: {:.1}%", result.match_percentage); @@ -162,8 +179,13 @@ fn test_vm_binary_comparison() -> Result<(), String> { } if binaries_found == 0 { - println!("⚠️ Warning: No ELF binaries found for any of the {} test cases", total_test_cases); - println!(" To build binaries, run: cargo build -p examples --release --target crates/compiler/targets/avm32.json --features binaries"); + println!( + "⚠️ Warning: No ELF binaries found for any of the {} test cases", + total_test_cases + ); + println!( + " To build binaries, run: cargo build -p examples --release --target crates/compiler/targets/avm32.json --features binaries" + ); println!(" Skipping binary comparison validation."); println!("\n✅ Test completed (skipped binary validation)"); cleanup(); @@ -178,7 +200,10 @@ fn test_vm_binary_comparison() -> Result<(), String> { )); } - println!("🎉 All {} found binaries matched 100% with VM execution!", binaries_found); + println!( + "🎉 All {} found binaries matched 100% with VM execution!", + binaries_found + ); println!("\n✅ Binary comparison test completed successfully!"); cleanup(); @@ -292,7 +317,11 @@ fn parse_instruction_line(line: &str) -> Option { let instr_part = line.split("Instr = ").nth(1)?; let mnemonic = instr_part.to_string(); - Some(Instruction { pc, bytes, mnemonic }) + Some(Instruction { + pc, + bytes, + mnemonic, + }) } fn calculate_match_percentage(instructions: &[Instruction]) -> f64 { diff --git a/crates/examples/tests/common/config.rs b/crates/examples/tests/common/config.rs new file mode 100644 index 0000000..e39b61b --- /dev/null +++ b/crates/examples/tests/common/config.rs @@ -0,0 +1,12 @@ +pub struct Config; + +impl Config { + pub const MAX_INPUT_LEN: usize = 1024; + pub const CODE_SIZE_LIMIT: usize = 0x30000; // 192KB headroom for non-compressed RV32IM binaries + pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; // 8KB for read-only data + pub const HEAP_START_ADDR: usize = Self::CODE_SIZE_LIMIT + Self::RO_DATA_SIZE_LIMIT + 0x100; + pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; + + pub const PROGRAM_START_ADDR: u32 = 0x400; + pub const RESULT_ADDR: u32 = 0x100; +} diff --git a/crates/examples/tests/common/ecdsa.rs b/crates/examples/tests/common/ecdsa.rs index aefb505..18d38f2 100644 --- a/crates/examples/tests/common/ecdsa.rs +++ b/crates/examples/tests/common/ecdsa.rs @@ -1,38 +1,28 @@ -use k256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey}; +use k256::ecdsa::{Signature, SigningKey, signature::hazmat::PrehashSigner}; // Fixed test private key (random-looking, non-trivial scalar) pub const ECDSA_SK_BYTES: [u8; 32] = [ - 0x79, 0x6d, 0x89, 0x3e, 0x8f, 0x16, 0x29, 0x5a, - 0xda, 0xfe, 0x04, 0x8c, 0x53, 0x2f, 0xf9, 0x7e, - 0x47, 0x22, 0x92, 0x1a, 0x86, 0xd2, 0xb4, 0x52, - 0x38, 0xa1, 0x6c, 0x9e, 0x1b, 0x45, 0xd3, 0x7c, + 0x79, 0x6d, 0x89, 0x3e, 0x8f, 0x16, 0x29, 0x5a, 0xda, 0xfe, 0x04, 0x8c, 0x53, 0x2f, 0xf9, 0x7e, + 0x47, 0x22, 0x92, 0x1a, 0x86, 0xd2, 0xb4, 0x52, 0x38, 0xa1, 0x6c, 0x9e, 0x1b, 0x45, 0xd3, 0x7c, ]; pub const ECDSA_HASH: [u8; 32] = [ - 0x3b, 0xbd, 0x38, 0x9e, 0x94, 0x1c, 0x63, 0x7f, - 0x36, 0x32, 0xaa, 0xf4, 0x2f, 0x93, 0xb7, 0xb1, - 0xf1, 0x7c, 0x6f, 0x31, 0x86, 0x92, 0x01, 0x34, - 0x1d, 0x5f, 0x28, 0x40, 0x61, 0x5c, 0xac, 0x2b, + 0x3b, 0xbd, 0x38, 0x9e, 0x94, 0x1c, 0x63, 0x7f, 0x36, 0x32, 0xaa, 0xf4, 0x2f, 0x93, 0xb7, 0xb1, + 0xf1, 0x7c, 0x6f, 0x31, 0x86, 0x92, 0x01, 0x34, 0x1d, 0x5f, 0x28, 0x40, 0x61, 0x5c, 0xac, 0x2b, ]; // Compressed SEC1 encoding of the corresponding public key pub const ECDSA_PK_BYTES: [u8; 33] = [ - 0x02, 0xda, 0x8c, 0x8e, 0x0a, 0x4e, 0x5d, 0xfc, - 0x76, 0x6f, 0xf1, 0xcb, 0xda, 0x27, 0x03, 0xea, - 0xcd, 0xb0, 0xdf, 0x07, 0xda, 0x19, 0xde, 0x65, - 0x03, 0x51, 0x46, 0xdb, 0x9b, 0x9c, 0x8a, 0xb7, + 0x02, 0xda, 0x8c, 0x8e, 0x0a, 0x4e, 0x5d, 0xfc, 0x76, 0x6f, 0xf1, 0xcb, 0xda, 0x27, 0x03, 0xea, + 0xcd, 0xb0, 0xdf, 0x07, 0xda, 0x19, 0xde, 0x65, 0x03, 0x51, 0x46, 0xdb, 0x9b, 0x9c, 0x8a, 0xb7, 0x0c, ]; // Deterministic signature over ECDSA_HASH with ECDSA_SK_BYTES (r || s), big-endian. pub const ECDSA_SIG_BYTES: [u8; 64] = [ - 0x13, 0xe3, 0x22, 0xb9, 0x33, 0x19, 0x17, 0x76, - 0x6d, 0x8c, 0xbf, 0xe9, 0x9f, 0x1d, 0x44, 0xd8, - 0xeb, 0x4f, 0x1d, 0xb3, 0xca, 0xd1, 0x31, 0xaf, - 0x92, 0xb2, 0xf2, 0x26, 0x3c, 0xe6, 0x60, 0x92, - 0x2a, 0x3a, 0xef, 0x94, 0xe6, 0x3e, 0x74, 0x06, - 0xf4, 0x20, 0xee, 0x0c, 0x0c, 0xb6, 0x5f, 0xce, - 0xe0, 0x45, 0x26, 0xba, 0x9e, 0x36, 0xf6, 0x20, - 0x92, 0x77, 0x73, 0x9d, 0x2d, 0x64, 0x37, 0xa2, + 0x13, 0xe3, 0x22, 0xb9, 0x33, 0x19, 0x17, 0x76, 0x6d, 0x8c, 0xbf, 0xe9, 0x9f, 0x1d, 0x44, 0xd8, + 0xeb, 0x4f, 0x1d, 0xb3, 0xca, 0xd1, 0x31, 0xaf, 0x92, 0xb2, 0xf2, 0x26, 0x3c, 0xe6, 0x60, 0x92, + 0x2a, 0x3a, 0xef, 0x94, 0xe6, 0x3e, 0x74, 0x06, 0xf4, 0x20, 0xee, 0x0c, 0x0c, 0xb6, 0x5f, 0xce, + 0xe0, 0x45, 0x26, 0xba, 0x9e, 0x36, 0xf6, 0x20, 0x92, 0x77, 0x73, 0x9d, 0x2d, 0x64, 0x37, 0xa2, ]; pub fn build_ecdsa_payload() -> Vec { diff --git a/crates/examples/tests/common/router.rs b/crates/examples/tests/common/router.rs new file mode 100644 index 0000000..6ee35d3 --- /dev/null +++ b/crates/examples/tests/common/router.rs @@ -0,0 +1,23 @@ +//! Minimal router helpers used by the host-side tests to encode calls. + +/// Represents a function call input for the VM router. +pub struct HostFuncCall { + pub selector: u8, + pub args: Vec, +} + +/// Encodes multiple function calls into a single buffer for the guest VM router. +pub fn encode_router_calls(calls: &[HostFuncCall]) -> Vec { + let mut encoded = Vec::new(); + + for call in calls { + let len = call.args.len(); + assert!(len <= 255, "argument too long for 1-byte length field"); + + encoded.push(call.selector); + encoded.push(len as u8); + encoded.extend_from_slice(&call.args); + } + + encoded +} diff --git a/crates/examples/tests/common/state.rs b/crates/examples/tests/common/state.rs index 6728a8a..c69d8a7 100644 --- a/crates/examples/tests/common/state.rs +++ b/crates/examples/tests/common/state.rs @@ -1,5 +1,5 @@ -use state::State; use super::utils::to_address; +use state::State; /// Build a test state with prefunded accounts. pub fn test_state() -> State { diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index af1b5f0..e66d0c0 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -25,7 +25,9 @@ impl FileWriter { impl Write for FileWriter { fn write_str(&mut self, s: &str) -> core::fmt::Result { - self.file.write_all(s.as_bytes()).map_err(|_| core::fmt::Error)?; + self.file + .write_all(s.as_bytes()) + .map_err(|_| core::fmt::Error)?; self.file.flush().map_err(|_| core::fmt::Error)?; Ok(()) } @@ -86,8 +88,8 @@ impl TestRunner { TestRunner { writer, verbose: false, - vm_memory_size: 512 * 1024, // larger default to accommodate bigger binaries without RVC - max_memory_pages: 128, // allow more pages for larger programs + vm_memory_size: 512 * 1024, // larger default to accommodate bigger binaries without RVC + max_memory_pages: 128, // allow more pages for larger programs kernel_bytes: Self::load_kernel_from_env(), kernel_path: env::var("KERNEL_ELF").ok(), } @@ -98,7 +100,12 @@ impl TestRunner { use super::TEST_CASES; writeln!(self.writer.borrow_mut(), "=== Starting Test Run ===").unwrap(); - writeln!(self.writer.borrow_mut(), "Verbose logging: {}", if self.verbose { "enabled" } else { "disabled" }).unwrap(); + writeln!( + self.writer.borrow_mut(), + "Verbose logging: {}", + if self.verbose { "enabled" } else { "disabled" } + ) + .unwrap(); for case in TEST_CASES.iter() { self.run_test_case(case)?; @@ -106,7 +113,12 @@ impl TestRunner { // Write test summary writeln!(self.writer.borrow_mut(), "\n=== Test Run Complete ===").unwrap(); - writeln!(self.writer.borrow_mut(), "Total test cases: {}", TEST_CASES.len()).unwrap(); + writeln!( + self.writer.borrow_mut(), + "Total test cases: {}", + TEST_CASES.len() + ) + .unwrap(); Ok(()) } @@ -116,9 +128,22 @@ impl TestRunner { let mut bootloader = Bootloader::new(self.max_memory_pages, self.vm_memory_size); // Write test case header - writeln!(self.writer.borrow_mut(), "\n############################################").unwrap(); - writeln!(self.writer.borrow_mut(), "#### Running test case: {} ####", case.name).unwrap(); - writeln!(self.writer.borrow_mut(), "############################################").unwrap(); + writeln!( + self.writer.borrow_mut(), + "\n############################################" + ) + .unwrap(); + writeln!( + self.writer.borrow_mut(), + "#### Running test case: {} ####", + case.name + ) + .unwrap(); + writeln!( + self.writer.borrow_mut(), + "############################################" + ) + .unwrap(); // Print address to binary mappings if !case.address_mappings.is_empty() { @@ -169,8 +194,8 @@ impl Default for TestRunner { impl TestRunner { fn load_kernel_from_env() -> Option> { - let path = env::var("KERNEL_ELF") - .unwrap_or_else(|_| "crates/os/bin/kernel.elf".to_string()); + let path = + env::var("KERNEL_ELF").unwrap_or_else(|_| "crates/os/bin/kernel.elf".to_string()); fs::read(&path).ok() } } diff --git a/crates/examples/tests/common/utils.rs b/crates/examples/tests/common/utils.rs index ebede90..6a0331b 100644 --- a/crates/examples/tests/common/utils.rs +++ b/crates/examples/tests/common/utils.rs @@ -1,10 +1,10 @@ -use types::address::Address; -use std::fs; -use std::path::Path; +use super::config::Config; use compiler::elf::parse_elf_from_bytes; -use avm::global::Config; -use compiler::{EventParam, EventAbi, ParamType}; +use compiler::{EventAbi, EventParam, ParamType}; use serde_json::Value; +use std::fs; +use std::path::Path; +use types::address::Address; pub fn to_address(hex: &str) -> Address { assert!(hex.len() == 40, "Hex string must be exactly 40 characters"); @@ -30,11 +30,19 @@ pub fn to_address(hex: &str) -> Address { } pub fn load_abi_from_file>(path: P) -> Option> { - let content = fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("❌ Failed to read ABI file from {}", path.as_ref().display())); - - let json: Value = serde_json::from_str(&content) - .unwrap_or_else(|_| panic!("❌ Failed to parse ABI JSON from {}", path.as_ref().display())); + let content = fs::read_to_string(&path).unwrap_or_else(|_| { + panic!( + "❌ Failed to read ABI file from {}", + path.as_ref().display() + ) + }); + + let json: Value = serde_json::from_str(&content).unwrap_or_else(|_| { + panic!( + "❌ Failed to parse ABI JSON from {}", + path.as_ref().display() + ) + }); let events = json.get("events")?; let events_array = events.as_array()?; @@ -48,7 +56,10 @@ pub fn load_abi_from_file>(path: P) -> Option> { for input in inputs { let param_name = input.get("name")?.as_str()?.to_string(); let param_type_str = input.get("type")?.as_str()?; - let indexed = input.get("indexed").and_then(|v| v.as_bool()).unwrap_or(false); + let indexed = input + .get("indexed") + .and_then(|v| v.as_bool()) + .unwrap_or(false); let param_type = match param_type_str { "address" => ParamType::Address, @@ -110,18 +121,22 @@ pub fn get_program_code(name: &str) -> Vec { .unwrap_or_else(|_| panic!("❌ Failed to parse ELF from {}", name)); let (code, code_start) = elf - .get_flat_code() - .unwrap_or_else(|| panic!("❌ No code sections found in ELF {}", name)); + .get_flat_code() + .unwrap_or_else(|| panic!("❌ No code sections found in ELF {}", name)); let (rodata, rodata_start) = elf .get_flat_rodata() - .unwrap_or_else(|| { - (vec![], usize::MAX as u64) - }); + .unwrap_or_else(|| (vec![], usize::MAX as u64)); // assert sizes - assert!(code.len() <= Config::CODE_SIZE_LIMIT, "code size exceeds limit"); - assert!(rodata.len() <= Config::RO_DATA_SIZE_LIMIT, "read only data size exceeds limit"); + assert!( + code.len() <= Config::CODE_SIZE_LIMIT, + "code size exceeds limit" + ); + assert!( + rodata.len() <= Config::RO_DATA_SIZE_LIMIT, + "read only data size exceeds limit" + ); let mut total_len = code_start + code.len() as u64; // assumes rodata is after code if rodata.len() > 0 { @@ -136,7 +151,8 @@ pub fn get_program_code(name: &str) -> Vec { // Copy rodata (if it exists) if rodata.len() > 0 { - combined[rodata_start as usize..rodata_start as usize + rodata.len()].copy_from_slice(&rodata); + combined[rodata_start as usize..rodata_start as usize + rodata.len()] + .copy_from_slice(&rodata); } combined } diff --git a/crates/examples/tests/ecdsa_payload_test.rs b/crates/examples/tests/ecdsa_payload_test.rs index d9365ce..d9982ed 100644 --- a/crates/examples/tests/ecdsa_payload_test.rs +++ b/crates/examples/tests/ecdsa_payload_test.rs @@ -1,4 +1,4 @@ -use k256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, SigningKey, VerifyingKey}; +use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::hazmat::PrehashVerifier}; #[path = "common/ecdsa.rs"] mod ecdsa; diff --git a/crates/examples/tests/examples_test.rs b/crates/examples/tests/examples_test.rs index a702360..62d8f4e 100644 --- a/crates/examples/tests/examples_test.rs +++ b/crates/examples/tests/examples_test.rs @@ -4,19 +4,25 @@ mod utils; #[path = "common/test_runner.rs"] mod test_runner; +#[path = "common/config.rs"] +mod config; + #[path = "common/state.rs"] mod state_helper; #[path = "common/ecdsa.rs"] mod ecdsa; -use avm::transaction::{TransactionType, TransactionBundle, Transaction}; -use avm::router::{encode_router_calls, HostFuncCall}; -use once_cell::sync::Lazy; +#[path = "common/router.rs"] +mod router; + +use router::{HostFuncCall, encode_router_calls}; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; use compiler::EventAbi; -pub use ecdsa::{build_ecdsa_payload, ECDSA_HASH, ECDSA_SK_BYTES}; +pub use ecdsa::{ECDSA_HASH, ECDSA_SK_BYTES, build_ecdsa_payload}; +use once_cell::sync::Lazy; pub use test_runner::TestRunner; -use utils::{to_address, load_abi_from_file, load_abis_from_files, get_program_code}; +use utils::{get_program_code, load_abi_from_file, load_abis_from_files, to_address}; /// Centralized ELF binary paths for testing pub struct ElfBinary { @@ -89,7 +95,6 @@ pub fn get_elf_path(name: &str) -> Option { get_elf_by_name(name).map(|elf| format!("crates/examples/{}", elf.path)) } - #[derive(Debug)] pub struct TestCase<'a> { pub name: &'a str, @@ -109,11 +114,9 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { expected_error_code: 0, expected_data: Some(vec![128, 240, 250, 2]), // Expected data: 50,000,000 in little-endian abi: load_abi_from_file("bin/erc20.abi.json"), - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "erc20"), - ], + address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "erc20")], bundle: TransactionBundle::new(vec![ - Transaction { + Transaction { tx_type: TransactionType::CreateAccount, from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), @@ -125,23 +128,21 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { tx_type: TransactionType::ProgramCall, to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x01, // initialize - args: (|| { - // max supply - let max_supply: u32 = 100000000; // 100 million - let mut max_supply_bytes: Vec = max_supply.to_le_bytes().to_vec(); + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, // initialize + args: (|| { + // max supply + let max_supply: u32 = 100000000; // 100 million + let mut max_supply_bytes: Vec = max_supply.to_le_bytes().to_vec(); - // decimals - let decimals: u8 = 18; + // decimals + let decimals: u8 = 18; - // combine - max_supply_bytes.extend(vec![decimals]); - max_supply_bytes - })(), - } - ]), + // combine + max_supply_bytes.extend(vec![decimals]); + max_supply_bytes + })(), + }]), value: 0, nonce: 0, }, @@ -149,22 +150,20 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { tx_type: TransactionType::ProgramCall, to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x02, // transfer - args: (|| { - // to address (20 bytes) - let to_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); - let mut args = to_addr.0.to_vec(); + data: encode_router_calls(&[HostFuncCall { + selector: 0x02, // transfer + args: (|| { + // to address (20 bytes) + let to_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); + let mut args = to_addr.0.to_vec(); - // amount (4 bytes) - let amount: u32 = 50000000; // 50 million tokens - args.extend(amount.to_le_bytes()); + // amount (4 bytes) + let amount: u32 = 50000000; // 50 million tokens + args.extend(amount.to_le_bytes()); - args - })(), - } - ]), + args + })(), + }]), value: 0, nonce: 0, }, @@ -172,22 +171,19 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { tx_type: TransactionType::ProgramCall, to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x05, // balance_of - args: (|| { - // check balance of the original caller (d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0) - let owner_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); - owner_addr.0.to_vec() - })(), - } - ]), + data: encode_router_calls(&[HostFuncCall { + selector: 0x05, // balance_of + args: (|| { + // check balance of the original caller (d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0) + let owner_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + owner_addr.0.to_vec() + })(), + }]), value: 0, nonce: 0, }, ]), }, - TestCase { name: "call program", expected_success: true, @@ -207,7 +203,7 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { value: 0, nonce: 0, }, - Transaction { + Transaction { tx_type: TransactionType::CreateAccount, from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), @@ -220,7 +216,9 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), data: (|| { - let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1").0.to_vec(); + let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1") + .0 + .to_vec(); data.extend(vec![100, 0, 0, 0, 42, 0, 0, 0]); data })(), @@ -229,16 +227,13 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { }, ]), }, - TestCase { name: "account create (storage)", expected_success: true, expected_error_code: 0, expected_data: None, abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "storage"), - ], + address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "storage")], bundle: TransactionBundle::new(vec![ Transaction { tx_type: TransactionType::CreateAccount, @@ -258,16 +253,13 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { }, ]), }, - TestCase { name: "account create (simple)", expected_success: true, expected_error_code: 0, expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "simple"), - ], + address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "simple")], bundle: TransactionBundle::new(vec![ Transaction { tx_type: TransactionType::CreateAccount, @@ -282,24 +274,21 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), data: vec![ - 100, 0, 0, 0, // first u64 = 100 - 42, 0, 0, 0, // second u64 = 42 + 100, 0, 0, 0, // first u64 = 100 + 42, 0, 0, 0, // second u64 = 42 ], value: 0, nonce: 0, }, ]), }, - TestCase { name: "multi function (simple)", expected_success: true, expected_error_code: 0, expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "multi_func"), - ], + address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "multi_func")], bundle: TransactionBundle::new(vec![ Transaction { tx_type: TransactionType::CreateAccount, @@ -313,30 +302,25 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { tx_type: TransactionType::ProgramCall, to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x01, - args: vec![ - 100, 0, 0, 0, // first = 100 - 42, 0, 0, 0, // second = 42 - ], - } - ]), + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: vec![ + 100, 0, 0, 0, // first = 100 + 42, 0, 0, 0, // second = 42 + ], + }]), value: 0, nonce: 0, }, ]), }, - TestCase { name: "allocator demo", expected_success: true, expected_error_code: 0, - expected_data: None,//Some(b"VM allocator demo completed successfully!".to_vec()), + expected_data: None, //Some(b"VM allocator demo completed successfully!".to_vec()), abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "allocator_demo"), - ], + address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "allocator_demo")], bundle: TransactionBundle::new(vec![ Transaction { tx_type: TransactionType::CreateAccount, @@ -353,19 +337,14 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { // 6 x u32 little-endian: // Vec: 12, 15, 100; Map: 95, 87, 92 data: vec![ - 12, 0, 0, 0, - 15, 0, 0, 0, - 100, 0, 0, 0, - 95, 0, 0, 0, - 87, 0, 0, 0, - 92, 0, 0, 0, + 12, 0, 0, 0, 15, 0, 0, 0, 100, 0, 0, 0, 95, 0, 0, 0, 87, 0, 0, 0, 92, 0, 0, + 0, ], value: 0, nonce: 0, }, ]), }, - TestCase { name: "native transfer", expected_success: true, @@ -373,18 +352,15 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { expected_data: None, abi: None, address_mappings: vec![], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::Transfer, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: vec![], - value: 10, - nonce: 0, - }, - ]), + bundle: TransactionBundle::new(vec![Transaction { + tx_type: TransactionType::Transfer, + to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), + from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), + data: vec![], + value: 10, + nonce: 0, + }]), }, - TestCase { name: "guest transfer syscall", expected_success: true, @@ -394,9 +370,10 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { v }), abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4", "native_transfer"), - ], + address_mappings: vec![( + "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4", + "native_transfer", + )], bundle: TransactionBundle::new(vec![ Transaction { tx_type: TransactionType::CreateAccount, @@ -411,7 +388,9 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), data: (|| { - let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0").0.to_vec(); + let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0") + .0 + .to_vec(); data.extend_from_slice(&42u64.to_le_bytes()); data })(), @@ -420,7 +399,6 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { }, ]), }, - TestCase { name: "dex amm", expected_success: true, @@ -449,18 +427,16 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { tx_type: TransactionType::ProgramCall, to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x01, // init - args: (|| { - let mut args = Vec::new(); - let supply: u32 = 1_000_000; - args.extend_from_slice(&supply.to_le_bytes()); - args.push(0); // decimals - args - })(), - } - ]), + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, // init + args: (|| { + let mut args = Vec::new(); + let supply: u32 = 1_000_000; + args.extend_from_slice(&supply.to_le_bytes()); + args.push(0); // decimals + args + })(), + }]), value: 0, nonce: 1, }, @@ -468,17 +444,17 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { tx_type: TransactionType::ProgramCall, to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: encode_router_calls(&[ - HostFuncCall { - selector: 0x02, // transfer - args: (|| { - let mut args = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5").0.to_vec(); - let amount: u32 = 500_000; - args.extend_from_slice(&amount.to_le_bytes()); - args - })(), - } - ]), + data: encode_router_calls(&[HostFuncCall { + selector: 0x02, // transfer + args: (|| { + let mut args = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5") + .0 + .to_vec(); + let amount: u32 = 500_000; + args.extend_from_slice(&amount.to_le_bytes()); + args + })(), + }]), value: 0, nonce: 2, }, @@ -533,16 +509,13 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { }, ]), }, - TestCase { name: "ecdsa verify", expected_success: true, expected_error_code: 0, expected_data: None, abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "ecdsa_verify"), - ], + address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "ecdsa_verify")], bundle: TransactionBundle::new(vec![ Transaction { tx_type: TransactionType::CreateAccount, @@ -562,7 +535,6 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { }, ]), }, - ] }); diff --git a/crates/os/Cargo.toml b/crates/os/Cargo.toml index 78c951e..a19cfa6 100644 --- a/crates/os/Cargo.toml +++ b/crates/os/Cargo.toml @@ -10,16 +10,11 @@ guest_kernel = [] [dependencies] types = { path = "../types" } -[target.'cfg(not(target_arch = "riscv32"))'.dependencies] +[target.'cfg(not(target_os = "none"))'.dependencies] vm = { path = "../vm" } -storage = { path = "../storage" } +state = { path = "../state" } goblin = "0.10" compiler = { path = "../compiler" } [target.'cfg(target_arch = "riscv32")'.dependencies] program = { path = "../program", default-features = false } - -[[bin]] -name = "kernel" -path = "src/kernel.rs" -required-features = ["guest_kernel"] diff --git a/crates/os/src/allocator.rs b/crates/os/src/allocator.rs deleted file mode 100644 index d832140..0000000 --- a/crates/os/src/allocator.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Minimal allocator used by the guest kernel. For riscv32 it delegates to -//! syscalls 7/8. -#![cfg(target_arch = "riscv32")] - -use core::alloc::{GlobalAlloc, Layout}; -use core::arch::asm; - -#[derive(Debug)] -pub struct VmAllocator; - -unsafe impl GlobalAlloc for VmAllocator { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - syscall_alloc(layout.size(), layout.align()) - } - - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - syscall_dealloc(ptr, layout.size()); - } -} - -unsafe fn syscall_alloc(size: usize, align: usize) -> *mut u8 { - let mut result: usize; - asm!( - "li a7, 7", // SYSCALL_ALLOC - "ecall", - in("a1") size, - in("a2") align, - out("a0") result, - ); - result as *mut u8 -} - -unsafe fn syscall_dealloc(ptr: *mut u8, size: usize) { - asm!( - "li a7, 8", // SYSCALL_DEALLOC - "ecall", - in("a1") ptr as usize, - in("a2") size, - options(nostack, preserves_flags), - ); -} diff --git a/crates/os/src/bin/bootloader_runner.rs b/crates/os/src/bin/bootloader_runner.rs index 86e897b..ef40191 100644 --- a/crates/os/src/bin/bootloader_runner.rs +++ b/crates/os/src/bin/bootloader_runner.rs @@ -2,8 +2,8 @@ use std::env; use std::fs; use os::bootloader::Bootloader; -use types::transaction::{Transaction, TransactionBundle, TransactionType}; use types::address::Address; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; fn main() { let kernel_path = env::args() diff --git a/crates/os/src/bootloader.rs b/crates/os/src/bootloader.rs index 3475e78..0dd9ff0 100644 --- a/crates/os/src/bootloader.rs +++ b/crates/os/src/bootloader.rs @@ -2,15 +2,15 @@ use std::cell::RefCell; use std::rc::Rc; use std::vec::Vec; -use types::transaction::TransactionBundle; use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; +use types::transaction::TransactionBundle; -use crate::memory::StackedMemory; use crate::DefaultSyscallHandler; -use storage::Storage; +use crate::memory::StackedMemory; +use state::State; use vm::host_interface::NoopHost; -use vm::memory::{Memory, HEAP_PTR_OFFSET}; +use vm::memory::{HEAP_PTR_OFFSET, Memory}; use vm::registers::Register; use vm::vm::VM; @@ -22,7 +22,9 @@ pub struct BootConfig { impl Default for BootConfig { fn default() -> Self { - Self { debug_console: true } + Self { + debug_console: true, + } } } @@ -49,18 +51,14 @@ impl Bootloader { .expect("failed to parse entry point") .entry as u32; - let (code, code_base) = elf - .get_flat_code() - .expect("kernel ELF missing .text"); + let (code, code_base) = elf.get_flat_code().expect("kernel ELF missing .text"); let (rodata, ro_base) = elf.get_flat_rodata().unwrap_or((Vec::new(), code_base)); let min_base = core::cmp::min(code_base, ro_base) as usize; let code_end = (code_base + code.len() as u64) as usize; let ro_end = (ro_base + rodata.len() as u64) as usize; let image_end = core::cmp::max(code_end, ro_end); - let image_size = image_end - .checked_sub(min_base) - .expect("invalid image size"); + let image_size = image_end.checked_sub(min_base).expect("invalid image size"); let page = self.memory.new_page(); assert!( @@ -87,15 +85,10 @@ impl Bootloader { /// AVM entry point where the kernel is responsible for invoking programs. pub fn execute_bundle(&mut self, kernel_elf: &[u8], bundle: &TransactionBundle) { let (entry_point, memory) = self.load_kernel(kernel_elf); - let storage = Rc::new(RefCell::new(Storage::new())); + let state = Rc::new(RefCell::new(State::new())); let host: Box = Box::new(NoopHost); - let mut vm = VM::new( - memory.clone(), - storage, - host, - Box::new(DefaultSyscallHandler::new()), - ); + let mut vm = VM::new(memory.clone(), host, Box::new(DefaultSyscallHandler::new(state))); self.place_bundle(&mut vm, bundle); vm.cpu.pc = entry_point; diff --git a/crates/os/src/lib.rs b/crates/os/src/lib.rs index dd220b9..c61d65f 100644 --- a/crates/os/src/lib.rs +++ b/crates/os/src/lib.rs @@ -8,15 +8,12 @@ //! Memory utilities are local copies of the VM page primitives to keep the OS //! independent from the execution engine. -#[cfg(target_arch = "riscv32")] -pub mod allocator; -#[cfg(not(target_arch = "riscv32"))] pub mod bootloader; -#[cfg(not(target_arch = "riscv32"))] + pub mod memory; -#[cfg(not(target_arch = "riscv32"))] -pub use vm::sys_call; -#[cfg(not(target_arch = "riscv32"))] + pub mod syscalls; -#[cfg(not(target_arch = "riscv32"))] + +pub use vm::sys_call; + pub use syscalls::DefaultSyscallHandler; diff --git a/crates/os/src/memory/memory_page.rs b/crates/os/src/memory/memory_page.rs index 5fafd1f..cba3ae0 100644 --- a/crates/os/src/memory/memory_page.rs +++ b/crates/os/src/memory/memory_page.rs @@ -1,8 +1,8 @@ use std::cell::{Cell, Ref, RefCell}; use std::convert::TryInto; use std::rc::Rc; -use vm::memory::{memory, HEAP_PTR_OFFSET}; -use vm::metering::{MeterResult, Metering, MemoryAccessKind}; +use vm::memory::{HEAP_PTR_OFFSET, memory}; +use vm::metering::{MemoryAccessKind, MeterResult, Metering}; #[derive(Debug, Clone)] pub struct MemoryPage { @@ -179,7 +179,9 @@ impl MemoryPage { if end_offset > mem_ref.len() || start_offset > end_offset { return None; } - Some(std::cell::Ref::map(mem_ref, move |v| &v[start_offset..end_offset])) + Some(std::cell::Ref::map(mem_ref, move |v| { + &v[start_offset..end_offset] + })) } pub fn write_code(&self, start_addr: usize, code: &[u8]) { diff --git a/crates/os/src/syscalls.rs b/crates/os/src/syscalls.rs index c116b47..3ff42c8 100644 --- a/crates/os/src/syscalls.rs +++ b/crates/os/src/syscalls.rs @@ -3,17 +3,18 @@ use core::fmt::Write; use std::any::Any; use std::rc::Rc; -use storage::Storage; -use types::result::RESULT_SIZE; +use state::State; +use types::{ADDRESS_LEN, address::Address, result::RESULT_SIZE}; use vm::host_interface::HostInterface; -use vm::memory::{Memory, HEAP_PTR_OFFSET}; +use vm::memory::{HEAP_PTR_OFFSET, Memory}; use vm::metering::{MeterResult, Metering}; use vm::registers::Register; use vm::sys_call::{ - SyscallHandler, SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, - SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, - SYSCALL_TRANSFER, + SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, + SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, + SyscallHandler, }; + /// Represents different types of arguments that can be passed to system calls. /// /// EDUCATIONAL: This enum demonstrates how to handle different data types @@ -30,6 +31,7 @@ enum Arg { pub struct DefaultSyscallHandler { verbose_writer: Option>>, + state: Rc>, } impl std::fmt::Debug for DefaultSyscallHandler { @@ -39,20 +41,26 @@ impl std::fmt::Debug for DefaultSyscallHandler { "verbose_writer", &self.verbose_writer.as_ref().map(|_| ""), ) + .field("state", &"") .finish() } } impl DefaultSyscallHandler { - pub fn new() -> Self { + pub fn new(state: Rc>) -> Self { Self { verbose_writer: None, + state, } } - pub fn with_writer(writer: Option>>) -> Self { + pub fn with_writer( + state: Rc>, + writer: Option>>, + ) -> Self { Self { verbose_writer: writer, + state, } } } @@ -63,7 +71,6 @@ impl SyscallHandler for DefaultSyscallHandler { call_id: u32, args: [u32; 6], memory: Memory, - storage: Rc>, host: &mut Box, regs: &mut [u32; 32], metering: &mut dyn Metering, @@ -72,8 +79,8 @@ impl SyscallHandler for DefaultSyscallHandler { panic!("Metering halted syscall {}", call_id); } let result = match call_id { - SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, storage, metering), - SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, storage, metering), + SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, metering), + SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, metering), SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), SYSCALL_LOG => self.sys_log(args, memory, metering), SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host, metering), @@ -129,15 +136,18 @@ impl DefaultSyscallHandler { &mut self, args: [u32; 6], memory: Memory, - storage: Rc>, metering: &mut dyn Metering, ) -> u32 { - let domain_ptr = args[0] as usize; - let domain_len = args[1] as usize; + let address_ptr = args[0] as usize; + let domain_ptr = args[1] as usize; let key_ptr = args[2] as usize; - let key_len = args[3] as usize; + let lens_packed = args[3] as usize; + let domain_len = lens_packed & 0xffff; + let key_len = lens_packed >> 16; - let total_len = domain_len.saturating_add(key_len); + let total_len = ADDRESS_LEN + .saturating_add(domain_len) + .saturating_add(key_len); if matches!( metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), MeterResult::Halt @@ -147,6 +157,28 @@ impl DefaultSyscallHandler { let borrowed_memory = memory.as_ref(); + // Parse address + let address_slice_ref = + match borrowed_memory.mem_slice(address_ptr, address_ptr + ADDRESS_LEN) { + Some(r) => r, + None => { + println!( + "❌ Storage GET - Invalid address memory access: ptr={}, len={}", + address_ptr, ADDRESS_LEN + ); + return 0; + } + }; + let address_bytes = address_slice_ref.as_ref(); + let address_hex = address_bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + let mut addr_arr = [0u8; ADDRESS_LEN]; + addr_arr.copy_from_slice(address_bytes); + let address = Address(addr_arr); + // Parse domain let domain_slice = { let domain_slice_ref = @@ -206,7 +238,14 @@ impl DefaultSyscallHandler { format!("{}:{}", domain, key) }; - if let Some(value) = storage.borrow().get(domain, &key) { + let value = { + let state_ref = self.state.borrow(); + state_ref + .get_account(&address) + .and_then(|acc| acc.storage.get(&format!("{}:{}", domain, key)).cloned()) + }; + + if let Some(value) = value { let mut buf = (value.len() as u32).to_le_bytes().to_vec(); buf.extend_from_slice(value.as_slice()); if matches!(metering.on_alloc(buf.len()), MeterResult::Halt) { @@ -214,14 +253,14 @@ impl DefaultSyscallHandler { } let addr = borrowed_memory.alloc_on_heap(&buf); println!( - "✅ Found value for domain: '{}', Key: '{}'", - domain, display_key + "✅ Found value for address: '{}', domain: '{}', Key: '{}'", + address_hex, domain, display_key ); return addr; } else { println!( - "❌ No value found for domain: '{}', key: '{}'", - domain, display_key + "❌ No value found for address: '{}', domain: '{}', key: '{}'", + address_hex, domain, display_key ); 0 } @@ -231,17 +270,22 @@ impl DefaultSyscallHandler { &mut self, args: [u32; 6], memory: Memory, - storage: Rc>, metering: &mut dyn Metering, ) -> u32 { - let domain_ptr = args[0] as usize; - let domain_len = args[1] as usize; + let address_ptr = args[0] as usize; + let domain_ptr = args[1] as usize; let key_ptr = args[2] as usize; - let key_len = args[3] as usize; + let lens_packed = args[3] as usize; let val_ptr = args[4] as usize; let val_len = args[5] as usize; - let total_len = domain_len.saturating_add(key_len).saturating_add(val_len); + let domain_len = lens_packed & 0xffff; + let key_len = lens_packed >> 16; + + let total_len = ADDRESS_LEN + .saturating_add(domain_len) + .saturating_add(key_len) + .saturating_add(val_len); if matches!( metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), MeterResult::Halt @@ -251,6 +295,28 @@ impl DefaultSyscallHandler { let borrowed_memory = memory.as_ref(); + // Parse address + let address_slice_ref = + match borrowed_memory.mem_slice(address_ptr, address_ptr + ADDRESS_LEN) { + Some(r) => r, + None => { + println!( + "❌ Storage SET - Invalid address memory access: ptr={}, len={}", + address_ptr, ADDRESS_LEN + ); + return 0; + } + }; + let address_bytes = address_slice_ref.as_ref(); + let address_hex = address_bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::>() + .join(""); + let mut addr_arr = [0u8; ADDRESS_LEN]; + addr_arr.copy_from_slice(address_bytes); + let address = Address(addr_arr); + // Parse domain let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { @@ -327,7 +393,12 @@ impl DefaultSyscallHandler { value_slice.len() ); - storage.borrow_mut().set(domain, &key, value_slice.to_vec()); + let composite_key = format!("{}:{}", domain, key); + self.state + .borrow_mut() + .get_account_mut(&address) + .storage + .insert(composite_key, value_slice.to_vec()); 0 } @@ -341,12 +412,7 @@ impl DefaultSyscallHandler { panic!("🔥 Guest panic: {}", msg); } - fn sys_log( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { + fn sys_log(&mut self, args: [u32; 6], memory: Memory, metering: &mut dyn Metering) -> u32 { let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; let payload_len = fmt_len.saturating_add(arg_len) as usize; if matches!( @@ -596,12 +662,7 @@ impl DefaultSyscallHandler { } } - fn sys_alloc( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { + fn sys_alloc(&mut self, args: [u32; 6], memory: Memory, metering: &mut dyn Metering) -> u32 { let size = args[0] as usize; // A0 register let align = args[1] as usize; // A1 register @@ -657,12 +718,7 @@ impl DefaultSyscallHandler { ptr } - fn sys_dealloc( - &mut self, - args: [u32; 6], - _memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { + fn sys_dealloc(&mut self, args: [u32; 6], _memory: Memory, metering: &mut dyn Metering) -> u32 { let size = args[1] as usize; if matches!(metering.on_alloc(size), MeterResult::Halt) { panic!("Metering halted SYSCALL_DEALLOC"); @@ -701,11 +757,7 @@ impl DefaultSyscallHandler { let mut to = [0u8; 20]; to.copy_from_slice(to_slice.as_ref()); - if host.transfer(to, value) { - 0 - } else { - 1 - } + if host.transfer(to, value) { 0 } else { 1 } } fn sys_balance( diff --git a/crates/os/tests/allocator_test.rs b/crates/os/tests/allocator_test.rs index 6a4cbff..60fc2ea 100644 --- a/crates/os/tests/allocator_test.rs +++ b/crates/os/tests/allocator_test.rs @@ -1,8 +1,8 @@ use os::memory::MemoryPage; use os::DefaultSyscallHandler; +use state::State; use std::cell::RefCell; use std::rc::Rc; -use storage::Storage; use vm::host_interface; use vm::memory::Memory; use vm::metering::NoopMeter; @@ -11,9 +11,9 @@ use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); + let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); + let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut meter = NoopMeter::default(); // Test SYSCALL_ALLOC @@ -23,7 +23,6 @@ fn test_allocator_syscalls() { SYSCALL_ALLOC, args, memory.clone(), - storage.clone(), &mut host, &mut regs, &mut meter, @@ -37,7 +36,6 @@ fn test_allocator_syscalls() { SYSCALL_DEALLOC, dealloc_args, memory.clone(), - storage.clone(), &mut host, &mut regs, &mut meter, @@ -49,9 +47,9 @@ fn test_allocator_syscalls() { #[test] fn test_multiple_allocations() { let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); + let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); + let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut regs = [0u32; 32]; let mut meter = NoopMeter::default(); @@ -66,7 +64,6 @@ fn test_multiple_allocations() { SYSCALL_ALLOC, args, memory.clone(), - storage.clone(), &mut host, &mut regs, &mut meter, @@ -92,9 +89,9 @@ fn test_multiple_allocations() { #[test] fn test_alignment_requirements() { let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); + let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); + let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut regs = [0u32; 32]; let mut meter = NoopMeter::default(); @@ -107,7 +104,6 @@ fn test_alignment_requirements() { SYSCALL_ALLOC, args, memory.clone(), - storage.clone(), &mut host, &mut regs, &mut meter, @@ -121,9 +117,9 @@ fn test_alignment_requirements() { #[test] fn test_invalid_alignment() { let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); + let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); + let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut regs = [0u32; 32]; let mut meter = NoopMeter::default(); @@ -136,7 +132,6 @@ fn test_invalid_alignment() { SYSCALL_ALLOC, args, memory.clone(), - storage.clone(), &mut host, &mut regs, &mut meter, diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml index f39abbb..6f4e8d8 100644 --- a/crates/program/Cargo.toml +++ b/crates/program/Cargo.toml @@ -6,6 +6,12 @@ edition = "2024" [features] default = ["guest_handlers"] guest_handlers = [] +guest_kernel = [] [dependencies] types = { path = "../types" } + +[[bin]] +name = "kernel" +path = "src/kernel.rs" +required-features = ["guest_kernel"] diff --git a/crates/os/src/kernel.rs b/crates/program/src/kernel.rs similarity index 62% rename from crates/os/src/kernel.rs rename to crates/program/src/kernel.rs index b5835ed..cd65de2 100644 --- a/crates/os/src/kernel.rs +++ b/crates/program/src/kernel.rs @@ -1,21 +1,11 @@ #![no_std] #![no_main] -extern crate alloc; - -#[cfg(target_arch = "riscv32")] -mod allocator; -use program::log; -use program::logf; - -use core::slice; use core::mem::forget; +use core::slice; +use program::{log, logf}; use types::transaction::{Transaction, TransactionBundle}; -#[cfg(target_arch = "riscv32")] -#[global_allocator] -static ALLOC: allocator::VmAllocator = allocator::VmAllocator; - /// Kernel entrypoint. Receives a pointer/length pair to an encoded `TransactionBundle` /// (produced by the bootloader) and walks each transaction. #[unsafe(no_mangle)] @@ -59,30 +49,3 @@ fn halt() -> ! { unsafe { core::arch::asm!("ebreak") }; loop {} } - -#[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { - #[cfg(target_arch = "riscv32")] - { - let msg_bytes = if let Some(s) = info.message().as_str() { - s.as_bytes() - } else { - b"kernel panic (non-str message)" - }; - unsafe { - core::arch::asm!( - "li a7, 3", // SYSCALL_PANIC - "ecall", - in("a0") msg_bytes.as_ptr(), - in("a1") msg_bytes.len(), - ); - core::arch::asm!("ebreak", options(nomem, nostack)); - } - loop {} - } - - #[cfg(not(target_arch = "riscv32"))] - { - panic!("kernel panic: {:?}", info); - } -} diff --git a/crates/program/src/storage.rs b/crates/program/src/storage.rs index 6a78d18..78c6e74 100644 --- a/crates/program/src/storage.rs +++ b/crates/program/src/storage.rs @@ -1,15 +1,15 @@ -use types::O; +use types::{O, address::Address}; /// Domain constant for persistent storage pub const PERSISTENT_DOMAIN: &str = "P"; /// Trait for persistent structs pub trait Persistent { - fn load() -> O + fn load(address: &Address) -> O where Self: Sized; - fn store(&self); + fn store(&self, address: &Address); } /// Macro that defines persistent structs with embedded static key @@ -58,21 +58,22 @@ macro_rules! persist_struct { } } - pub fn load() -> $crate::types::O { - <$name as $crate::Persistent>::load() + pub fn load(address: &$crate::types::address::Address) -> $crate::types::O { + <$name as $crate::Persistent>::load(address) } - pub fn store(&self) { - <$name as $crate::Persistent>::store(self) + pub fn store(&self, address: &$crate::types::address::Address) { + <$name as $crate::Persistent>::store(self, address) } } impl $crate::Persistent for $name { - fn load() -> $crate::types::O { + fn load(address: &$crate::types::address::Address) -> $crate::types::O { #[cfg(target_arch = "riscv32")] unsafe { let key_ptr = $name::key_ptr(); let key_len = $name::key_len(); + let packed_lens: u32 = ((key_len as u32) << 16) | ($crate::PERSISTENT_DOMAIN.len() as u32); if key_len == 0 { $crate::vm_panic( @@ -84,10 +85,10 @@ macro_rules! persist_struct { core::arch::asm!( "li a7, 1", // syscall_storage_read "ecall", - in("a1") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant - in("a2") $crate::PERSISTENT_DOMAIN.len(), // domain len + in("a1") address.as_ref().as_ptr(), // address ptr + in("a2") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant in("a3") key_ptr, // key ptr - in("a4") key_len, // key len + in("a4") packed_lens, // packed lens (domain | key) out("a0") value_ptr, ); @@ -106,7 +107,7 @@ macro_rules! persist_struct { if value_len == 0 { $crate::require(value_len > 0, b"Decoded value len is 0 for bytes"); return $crate::types::O::None; - } + } let data_ptr = (value_ptr + 4) as *const u8; let value_buf = core::slice::from_raw_parts(data_ptr, value_len); @@ -116,16 +117,18 @@ macro_rules! persist_struct { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, return None $crate::types::O::None } } - fn store(&self) { + fn store(&self, address: &$crate::types::address::Address) { #[cfg(target_arch = "riscv32")] unsafe { let key_ptr = $name::key_ptr(); let key_len = $name::key_len(); + let packed_lens: u32 = ((key_len as u32) << 16) | ($crate::PERSISTENT_DOMAIN.len() as u32); if key_len == 0 { $crate::vm_panic( @@ -145,10 +148,10 @@ macro_rules! persist_struct { core::arch::asm!( "li a7, 2", // syscall_storage_write "ecall", - in("a1") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant - in("a2") $crate::PERSISTENT_DOMAIN.len(), // domain len + in("a1") address.as_ref().as_ptr(), // address ptr + in("a2") $crate::PERSISTENT_DOMAIN.as_ptr(), // domain ptr - use constant in("a3") key_ptr, // key ptr - in("a4") key_len, // key len + in("a4") packed_lens, // packed lens (domain | key) in("a5") val_ptr, // value ptr in("a6") val_len, // value len options(readonly, nostack, preserves_flags) @@ -157,6 +160,7 @@ macro_rules! persist_struct { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, do nothing } } diff --git a/crates/program/src/storage_map.rs b/crates/program/src/storage_map.rs index 6f84259..f05e736 100644 --- a/crates/program/src/storage_map.rs +++ b/crates/program/src/storage_map.rs @@ -1,6 +1,5 @@ -use core::mem::{size_of, MaybeUninit}; use crate::{require, types::O, types::address::Address}; -use crate::logf; +use core::mem::{MaybeUninit, size_of}; /// Trait for types that can be used as storage keys in `StorageMap`. pub trait StorageKey { @@ -15,11 +14,10 @@ impl StorageKey for Address { } } - pub struct StorageMap; impl StorageMap { - pub fn get(domain: &[u8], key: &[u8]) -> O + pub fn get(address: &Address, domain: &[u8], key: &[u8]) -> O where V: Copy + Default, { @@ -31,14 +29,15 @@ impl StorageMap { #[cfg(target_arch = "riscv32")] unsafe { + let packed_lens: u32 = ((key.len() as u32) << 16) | (domain.len() as u32); let mut value_ptr: u32; core::arch::asm!( "li a7, 1", // syscall_storage_read "ecall", - in("a1") domain.as_ptr(), // a1 - domain ptr - in("a2") domain.len(), // a2 - domain len + in("a1") address.as_ref().as_ptr(), // a1 - address ptr + in("a2") domain.as_ptr(), // a2 - domain ptr in("a3") full_key.as_ptr(), // a3 - key ptr - in("a4") key.len(), // a4 - key len + in("a4") packed_lens, // a4 - packed lens (domain | key) out("a0") value_ptr, // a0 ); @@ -63,12 +62,13 @@ impl StorageMap { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, return None O::None } } - pub fn set(domain: &[u8], key: &[u8], val: V) + pub fn set(address: &Address, domain: &[u8], key: &[u8], val: V) where V: Copy, { @@ -78,19 +78,19 @@ impl StorageMap { let mut full_key = [0u8; 64]; full_key[..key.len()].copy_from_slice(key); - let val_bytes = unsafe { - core::slice::from_raw_parts((&val as *const V) as *const u8, size_of::()) - }; - + let val_bytes = + unsafe { core::slice::from_raw_parts((&val as *const V) as *const u8, size_of::()) }; + #[cfg(target_arch = "riscv32")] unsafe { + let packed_lens: u32 = ((key.len() as u32) << 16) | (domain.len() as u32); core::arch::asm!( "li a7, 2", // syscall_storage_write "ecall", - in("a1") domain.as_ptr(), // a1 - domain ptr - in("a2") domain.len(), // a2 - domain len + in("a1") address.as_ref().as_ptr(), // a1 - address ptr + in("a2") domain.as_ptr(), // a2 - domain ptr in("a3") full_key.as_ptr(), // a3 - key ptr - in("a4") key.len(), // a4 - key len + in("a4") packed_lens, // a4 - packed lens (domain | key) in("a5") val_bytes.as_ptr(), // a5 - value ptr in("a6") val_bytes.len(), // a6 - value len options(readonly, nostack, preserves_flags) @@ -99,12 +99,12 @@ impl StorageMap { #[cfg(not(target_arch = "riscv32"))] { + let _ = address; // For non-RISC-V targets, do nothing } } } - #[macro_export] macro_rules! Map { ($name:ident) => { @@ -131,30 +131,37 @@ macro_rules! Map { key_len } - pub fn get(key: K) -> $crate::types::O + pub fn get( + address: &$crate::types::address::Address, + key: K, + ) -> $crate::types::O where K: $crate::StorageKey, V: Copy + Default, { let mut buf = [0u8; Self::MAX_KEY_LEN]; let total_len = Self::build_key(key, &mut buf); - $crate::StorageMap::get::(Self::DOMAIN_NAME.as_bytes(), &buf[..total_len]) + $crate::StorageMap::get::( + address, + Self::DOMAIN_NAME.as_bytes(), + &buf[..total_len], + ) } - pub fn set(key: K, val: V) + pub fn set(address: &$crate::types::address::Address, key: K, val: V) where K: $crate::StorageKey, V: Copy, { let mut buf = [0u8; Self::MAX_KEY_LEN]; let total_len = Self::build_key(key, &mut buf); - $crate::StorageMap::set::(Self::DOMAIN_NAME.as_bytes(), &buf[..total_len], val); + $crate::StorageMap::set::( + address, + Self::DOMAIN_NAME.as_bytes(), + &buf[..total_len], + val, + ); } } }; } - - - - - diff --git a/crates/types/src/address.rs b/crates/types/src/address.rs index f6ee483..8dc9be3 100644 --- a/crates/types/src/address.rs +++ b/crates/types/src/address.rs @@ -2,6 +2,8 @@ use core::fmt; use crate::O; use crate::SerializeField; +pub const ADDRESS_LEN: usize = 20; + #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(C)] pub struct Address(pub [u8; 20]); @@ -56,4 +58,4 @@ impl SerializeField for Address { panic!("Buffer overflow in Address serialization"); } } -} \ No newline at end of file +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index d46eb86..bad15fd 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -3,7 +3,7 @@ extern crate alloc; pub mod address; -pub use address::Address; +pub use address::{Address, ADDRESS_LEN}; pub mod result; pub use result::Result; diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 211bc7d..92d3d14 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] state = { path = "../state" } # adjust path as needed -storage = { path = "../storage" } # adjust path as needed types = { path = "../types" } # adjust path as needed [features] diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index d418855..d42d500 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -8,7 +8,6 @@ use core::cell::RefCell; use core::fmt::Write; use std::collections::HashMap; use std::rc::Rc; -use storage::Storage; #[path = "exe.rs"] mod exec; @@ -38,9 +37,8 @@ mod exec; /// different hardware (like x86 or ARM). The VM provides an abstraction layer /// that makes the underlying hardware details transparent to the running program. /// -/// MEMORY MANAGEMENT: We use Rc-backed trait objects for shared memory and -/// Rc> for storage, which allows the CPU to read/write memory -/// while maintaining Rust's safety guarantees. +/// MEMORY MANAGEMENT: We use Rc-backed trait objects for shared memory, which +/// allows the CPU to read/write memory while maintaining Rust's safety guarantees. /// /// PERFORMANCE CONSIDERATIONS: This is an interpretive VM, meaning each /// instruction is decoded and executed one at a time. Real CPUs use techniques @@ -204,19 +202,14 @@ impl CPU { /// /// RETURN VALUE: Returns true if execution should continue, false to halt /// - /// MEMORY ACCESS: Uses shared references to memory and storage to allow + /// MEMORY ACCESS: Uses shared references to memory to allow /// the CPU to read/write while maintaining Rust's safety guarantees. /// /// REAL-WORLD ANALOGY: This is like a factory assembly line where each /// worker (instruction) performs a specific task. The conveyor belt (PC) /// moves to the next task automatically, unless a task specifically /// redirects the flow (like a branch or jump instruction). - pub fn step( - &mut self, - memory: Memory, - storage: Rc>, - host: &mut Box, - ) -> bool { + pub fn step(&mut self, memory: Memory, host: &mut Box) -> bool { // EDUCATIONAL: Step 1 - Fetch and decode the next instruction let instr = self.next_instruction(Rc::clone(&memory)); @@ -224,11 +217,11 @@ impl CPU { match instr { Some((instr, size)) => { // Valid instruction found - execute it - self.run_instruction(instr, size, Rc::clone(&memory), storage, host) + self.run_instruction(instr, size, Rc::clone(&memory), host) } None => { // No valid instruction found - handle the error - self.unknown_instruction(Rc::clone(&memory), storage) + self.unknown_instruction(Rc::clone(&memory)) } } } @@ -246,13 +239,11 @@ impl CPU { /// - instr: The decoded instruction to execute /// - size: Size of the instruction in bytes (2 for compressed, 4 for full) /// - memory: Shared reference to memory for load/store operations - /// - storage: Shared reference to persistent storage fn run_instruction( &mut self, instr: Instruction, size: u8, memory: Memory, - storage: Rc>, host: &mut Box, ) -> bool { // EDUCATIONAL: Debug output to help understand what's happening @@ -287,7 +278,7 @@ impl CPU { let old_pc = self.pc; // EDUCATIONAL: Execute the instruction - let result = self.execute(instr, memory, storage, host); + let result = self.execute(instr, memory, host); // EDUCATIONAL: Only increment PC if the instruction didn't change it // This handles branches, jumps, and calls correctly @@ -309,11 +300,7 @@ impl CPU { /// went wrong, including the hex dump of the invalid bytes. /// /// RETURN VALUE: Returns false to halt execution on invalid instructions - fn unknown_instruction( - &mut self, - memory: Memory, - _storage: Rc>, - ) -> bool { + fn unknown_instruction(&mut self, memory: Memory) -> bool { // EDUCATIONAL: Try to read the invalid instruction bytes for debugging if let Some(slice_ref) = memory.mem_slice(self.pc as usize, self.pc as usize + 4) { // EDUCATIONAL: Convert bytes to hex for human-readable debugging diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index bb96b5d..4c70ebd 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,11 +1,7 @@ -use core::cell::RefCell; -use std::rc::Rc; - use super::{Instruction, MemoryAccessKind, Memory, CPU}; use crate::host_interface::HostInterface; use crate::instruction::CsrOp; use crate::registers::Register; -use storage::Storage; impl CPU { /// Executes a decoded instruction. @@ -31,7 +27,6 @@ impl CPU { &mut self, instr: Instruction, memory: Memory, - storage: Rc>, host: &mut Box, ) -> bool { match instr { @@ -800,7 +795,6 @@ impl CPU { call_id, args, memory, - storage, host, &mut self.regs, self.metering.as_mut(), diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index a723b8e..b4ca712 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -2,9 +2,6 @@ use crate::host_interface::HostInterface; use crate::memory::Memory; use crate::metering::Metering; use core::any::Any; -use core::cell::RefCell; -use std::rc::Rc; -use storage::Storage; /// System call IDs for the VM. pub const SYSCALL_STORAGE_GET: u32 = 1; @@ -25,7 +22,6 @@ pub trait SyscallHandler: std::fmt::Debug { call_id: u32, args: [u32; 6], memory: Memory, - storage: Rc>, host: &mut Box, regs: &mut [u32; 32], metering: &mut dyn Metering, diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index e71125d..0c51bfd 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -4,25 +4,22 @@ use crate::memory::Memory; use crate::metering::Metering; use crate::registers::Register; use crate::sys_call::SyscallHandler; -use core::cell::RefCell; use std::rc::Rc; -use storage::Storage; /// Represents a complete RISC-V virtual machine. /// /// EDUCATIONAL PURPOSE: This struct encapsulates all the components needed -/// to run a virtual machine: CPU, memory, and persistent storage. It provides +/// to run a virtual machine: CPU and memory. It provides /// a high-level interface for VM operations while hiding the complexity of /// the underlying components. /// /// VM ARCHITECTURE OVERVIEW: /// - CPU: Executes RISC-V instructions /// - Memory: Provides RAM for the running program -/// - Storage: Persistent storage for data that survives between runs /// -/// MEMORY MANAGEMENT: Uses Rc to share a trait-backed memory implementation -/// and Rc> for persistent storage, allowing the VM to manage -/// resources efficiently while maintaining Rust's safety guarantees. +/// MEMORY MANAGEMENT: Uses Rc to share a trait-backed memory implementation, +/// allowing the VM to manage resources efficiently while maintaining Rust's +/// safety guarantees. #[derive(Debug)] pub struct VM { /// The CPU that executes RISC-V instructions @@ -31,28 +28,23 @@ pub struct VM { /// Shared reference to the VM's memory (RAM) pub memory: Memory, - /// Shared reference to persistent storage - pub storage: Rc>, - pub host: Box, } impl VM { - /// Creates a new virtual machine with the specified memory, storage, host, and syscall handler. + /// Creates a new virtual machine with the specified memory, host, and syscall handler. pub fn new( memory: Memory, - storage: Rc>, host: Box, syscall_handler: Box, ) -> Self { - Self::new_with_syscall_handler(memory, storage, host, syscall_handler) + Self::new_with_syscall_handler(memory, host, syscall_handler) } /// Creates a new virtual machine with a custom syscall handler. /// This is useful for testing or custom environments. pub fn new_with_syscall_handler( memory: Memory, - storage: Rc>, host: Box, syscall_handler: Box, ) -> Self { @@ -61,7 +53,6 @@ impl VM { Self { cpu, memory, - storage, host, } } @@ -257,10 +248,6 @@ impl VM { /// call this after setting up the initial state. pub fn raw_run(&mut self) { // EDUCATIONAL: Main execution loop - fetch, decode, execute - while self.cpu.step( - Rc::clone(&self.memory), - Rc::clone(&self.storage), - &mut self.host, - ) {} + while self.cpu.step(Rc::clone(&self.memory), &mut self.host) {} } } From 3e4c619d182088b32586302dcbd063c9f9b6c974 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 19 Dec 2025 17:25:20 +0100 Subject: [PATCH 10/70] Write bundle into memory before running kernel --- crates/os/src/bootloader.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/os/src/bootloader.rs b/crates/os/src/bootloader.rs index 0dd9ff0..42576bc 100644 --- a/crates/os/src/bootloader.rs +++ b/crates/os/src/bootloader.rs @@ -89,10 +89,9 @@ impl Bootloader { let host: Box = Box::new(NoopHost); let mut vm = VM::new(memory.clone(), host, Box::new(DefaultSyscallHandler::new(state))); - self.place_bundle(&mut vm, bundle); vm.cpu.pc = entry_point; - // TODO: write the bundle into memory for the kernel to consume. + self.place_bundle(&mut vm, bundle); vm.raw_run(); } From 99428030488e807543df5527b34206422e052bcf Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 19 Dec 2025 17:29:40 +0100 Subject: [PATCH 11/70] Stage pending changes --- Cargo.lock | 28 ++++++++++----------- crates/avm/Cargo.toml | 2 +- crates/avm/src/avm.rs | 2 +- crates/avm/tests/allocator_test.rs | 2 +- crates/examples/Cargo.toml | 2 +- crates/examples/tests/common/test_runner.rs | 2 +- crates/os/Cargo.toml | 2 +- crates/os/src/bin/bootloader_runner.rs | 2 +- crates/os/tests/allocator_test.rs | 4 +-- crates/os/tests/memory_page_offset.rs | 2 +- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 293ffe8..3ac9114 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,9 +6,9 @@ version = 4 name = "avm" version = "0.1.0" dependencies = [ + "bootloader", "compiler", "hex", - "os", "state", "storage", "types", @@ -36,6 +36,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bootloader" +version = "0.1.0" +dependencies = [ + "compiler", + "goblin", + "program", + "state", + "types", + "vm", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -145,10 +157,10 @@ dependencies = [ name = "examples" version = "0.1.0" dependencies = [ + "bootloader", "compiler", "k256", "once_cell", - "os", "program", "serde_json", "sha2", @@ -256,18 +268,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "os" -version = "0.1.0" -dependencies = [ - "compiler", - "goblin", - "program", - "state", - "types", - "vm", -] - [[package]] name = "pkcs8" version = "0.10.2" diff --git a/crates/avm/Cargo.toml b/crates/avm/Cargo.toml index ada2c94..48d0eae 100644 --- a/crates/avm/Cargo.toml +++ b/crates/avm/Cargo.toml @@ -10,4 +10,4 @@ state = { path = "../state" } # adjust path as needed storage = { path = "../storage" } # adjust path as needed vm = { path = "../vm" } # adjust path as needed types = { path = "../types" } # adjust path as needed -os = { path = "../os" } +bootloader = { path = "../os" } diff --git a/crates/avm/src/avm.rs b/crates/avm/src/avm.rs index b2744df..ec82e44 100644 --- a/crates/avm/src/avm.rs +++ b/crates/avm/src/avm.rs @@ -18,7 +18,7 @@ use types::address::Address; use types::result::Result; use vm::registers::Register; use vm::vm::VM; -use os::DefaultSyscallHandler; +use bootloader::DefaultSyscallHandler; /// Application Virtual Machine (AVM) - the main orchestrator for smart contract execution. /// diff --git a/crates/avm/tests/allocator_test.rs b/crates/avm/tests/allocator_test.rs index 3a8497c..0052491 100644 --- a/crates/avm/tests/allocator_test.rs +++ b/crates/avm/tests/allocator_test.rs @@ -5,7 +5,7 @@ use storage::Storage; use vm::host_interface; use vm::metering::NoopMeter; use vm::memory::Memory; -use os::DefaultSyscallHandler; +use bootloader::DefaultSyscallHandler; use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 66f2e25..a6811bb 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -12,7 +12,7 @@ k256 = { version = "0.13", default-features = false, features = ["arithmetic", " types = { path = "../types" } # adjust path as needed compiler = { path = "../compiler" } # adjust path as needed state = { path = "../state" } -os = { path = "../os" } +bootloader = { path = "../os" } once_cell = "1.19.0" serde_json = "1.0" diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index e66d0c0..c1d7eaa 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use core::cell::RefCell; use core::fmt::Write; -use os::bootloader::Bootloader; +use bootloader::bootloader::Bootloader; // File writer for logging to disk struct FileWriter { diff --git a/crates/os/Cargo.toml b/crates/os/Cargo.toml index a19cfa6..ce9074c 100644 --- a/crates/os/Cargo.toml +++ b/crates/os/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "os" +name = "bootloader" version = "0.1.0" edition = "2024" readme = "README.md" diff --git a/crates/os/src/bin/bootloader_runner.rs b/crates/os/src/bin/bootloader_runner.rs index ef40191..892a088 100644 --- a/crates/os/src/bin/bootloader_runner.rs +++ b/crates/os/src/bin/bootloader_runner.rs @@ -1,7 +1,7 @@ use std::env; use std::fs; -use os::bootloader::Bootloader; +use bootloader::bootloader::Bootloader; use types::address::Address; use types::transaction::{Transaction, TransactionBundle, TransactionType}; diff --git a/crates/os/tests/allocator_test.rs b/crates/os/tests/allocator_test.rs index 60fc2ea..75b1e7c 100644 --- a/crates/os/tests/allocator_test.rs +++ b/crates/os/tests/allocator_test.rs @@ -1,5 +1,5 @@ -use os::memory::MemoryPage; -use os::DefaultSyscallHandler; +use bootloader::memory::MemoryPage; +use bootloader::DefaultSyscallHandler; use state::State; use std::cell::RefCell; use std::rc::Rc; diff --git a/crates/os/tests/memory_page_offset.rs b/crates/os/tests/memory_page_offset.rs index df10b9d..74fdcaa 100644 --- a/crates/os/tests/memory_page_offset.rs +++ b/crates/os/tests/memory_page_offset.rs @@ -1,4 +1,4 @@ -use os::memory::MemoryPage; +use bootloader::memory::MemoryPage; use vm::metering::{MemoryAccessKind, NoopMeter}; #[test] From 33a6c813c8e523a612f9f45ef4515e18038c39b9 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 19 Dec 2025 20:18:11 +0100 Subject: [PATCH 12/70] Kernel state plumbing and no_std cleanup --- Cargo.lock | 1 + .../examples/tests/binary_comparison_test.rs | 3 - crates/examples/tests/common/test_runner.rs | 22 +-- crates/examples/tests/common/utils.rs | 2 +- crates/examples/tests/examples_test.rs | 3 - crates/os/src/bin/bootloader_runner.rs | 26 ---- crates/os/src/bootloader.rs | 19 ++- crates/os/src/syscalls.rs | 23 ++- crates/program/Cargo.toml | 1 + .../tests/common => program/src}/config.rs | 0 crates/program/src/kernel.rs | 104 ++++++++++++-- crates/program/src/lib.rs | 3 + crates/state/Cargo.toml | 7 +- crates/state/src/account.rs | 4 +- crates/state/src/lib.rs | 8 +- crates/state/src/state.rs | 132 +++++++++++++++++- crates/storage/src/lib.rs | 16 ++- crates/types/src/address.rs | 2 +- crates/vm/src/sys_call.rs | 1 + 19 files changed, 286 insertions(+), 91 deletions(-) delete mode 100644 crates/os/src/bin/bootloader_runner.rs rename crates/{examples/tests/common => program/src}/config.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 3ac9114..28662d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,7 @@ dependencies = [ name = "program" version = "0.1.0" dependencies = [ + "state", "types", ] diff --git a/crates/examples/tests/binary_comparison_test.rs b/crates/examples/tests/binary_comparison_test.rs index 2525ad5..34c932a 100644 --- a/crates/examples/tests/binary_comparison_test.rs +++ b/crates/examples/tests/binary_comparison_test.rs @@ -8,9 +8,6 @@ use std::rc::Rc; #[path = "examples_test.rs"] mod examples_test; -#[path = "common/config.rs"] -mod config; - #[path = "common/utils.rs"] mod utils; diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index c1d7eaa..ccf1dac 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use core::cell::RefCell; use core::fmt::Write; use bootloader::bootloader::Bootloader; +use state::State; // File writer for logging to disk struct FileWriter { @@ -126,6 +127,7 @@ impl TestRunner { /// Run a single test case fn run_test_case(&self, case: &super::TestCase) -> Result<(), String> { let mut bootloader = Bootloader::new(self.max_memory_pages, self.vm_memory_size); + let state = Rc::new(RefCell::new(State::new())); // Write test case header writeln!( @@ -154,31 +156,13 @@ impl TestRunner { } writeln!(self.writer.borrow_mut()).unwrap(); - // Load kernel via bootloader before executing transactions with the AVM path. - if let Some(kernel) = &self.kernel_bytes { - writeln!( - self.writer.borrow_mut(), - "🚀 Booting kernel ELF {} via bootloader...", - self.kernel_path - .as_deref() - .unwrap_or("") - ) - .unwrap(); - bootloader.execute_bundle(kernel, &case.bundle); - } else { - writeln!( - self.writer.borrow_mut(), - "⚠️ KERNEL_ELF not set or unreadable; skipping bootloader run." - ) - .unwrap(); - } - // Execute the whole bundle via the bootloader/kernel path. bootloader.execute_bundle( self.kernel_bytes.as_ref().ok_or_else(|| { "KERNEL_ELF not set or unreadable; bootloader path required".to_string() })?, &case.bundle, + state, ); // For now we treat successful bootloader execution as a passed test. diff --git a/crates/examples/tests/common/utils.rs b/crates/examples/tests/common/utils.rs index 6a0331b..662cf7b 100644 --- a/crates/examples/tests/common/utils.rs +++ b/crates/examples/tests/common/utils.rs @@ -1,4 +1,4 @@ -use super::config::Config; +use program::Config; use compiler::elf::parse_elf_from_bytes; use compiler::{EventAbi, EventParam, ParamType}; use serde_json::Value; diff --git a/crates/examples/tests/examples_test.rs b/crates/examples/tests/examples_test.rs index 62d8f4e..9cc4c2b 100644 --- a/crates/examples/tests/examples_test.rs +++ b/crates/examples/tests/examples_test.rs @@ -4,9 +4,6 @@ mod utils; #[path = "common/test_runner.rs"] mod test_runner; -#[path = "common/config.rs"] -mod config; - #[path = "common/state.rs"] mod state_helper; diff --git a/crates/os/src/bin/bootloader_runner.rs b/crates/os/src/bin/bootloader_runner.rs deleted file mode 100644 index 892a088..0000000 --- a/crates/os/src/bin/bootloader_runner.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::env; -use std::fs; - -use bootloader::bootloader::Bootloader; -use types::address::Address; -use types::transaction::{Transaction, TransactionBundle, TransactionType}; - -fn main() { - let kernel_path = env::args() - .nth(1) - .expect("pass a path to the kernel ELF as the first argument"); - let kernel_bytes = fs::read(&kernel_path).expect("failed to read kernel ELF"); - - // Temporary: build a single transfer transaction for the bundle. - let bundle = TransactionBundle::new(vec![Transaction { - tx_type: TransactionType::Transfer, - to: Address([0u8; 20]), - from: Address([1u8; 20]), - data: Vec::new(), - value: 0, - nonce: 0, - }]); - - let mut bootloader = Bootloader::new(4, 4096); - bootloader.execute_bundle(&kernel_bytes, &bundle); -} diff --git a/crates/os/src/bootloader.rs b/crates/os/src/bootloader.rs index 42576bc..cc1efad 100644 --- a/crates/os/src/bootloader.rs +++ b/crates/os/src/bootloader.rs @@ -83,15 +83,21 @@ impl Bootloader { /// Execute a transaction bundle by delegating to the kernel. This mirrors the /// AVM entry point where the kernel is responsible for invoking programs. - pub fn execute_bundle(&mut self, kernel_elf: &[u8], bundle: &TransactionBundle) { + pub fn execute_bundle( + &mut self, + kernel_elf: &[u8], + bundle: &TransactionBundle, + state: Rc>, + ) { let (entry_point, memory) = self.load_kernel(kernel_elf); - let state = Rc::new(RefCell::new(State::new())); let host: Box = Box::new(NoopHost); - let mut vm = VM::new(memory.clone(), host, Box::new(DefaultSyscallHandler::new(state))); + let mut vm = VM::new(memory.clone(), host, Box::new(DefaultSyscallHandler::new(state.clone()))); vm.cpu.pc = entry_point; self.place_bundle(&mut vm, bundle); + let encoded_state = state.borrow().encode(); + self.place_state(&mut vm, &encoded_state); vm.raw_run(); } @@ -104,4 +110,11 @@ impl Bootloader { vm.memory .set_next_heap((addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32); } + + fn place_state(&mut self, vm: &mut VM, state: &[u8]) { + let addr = vm.set_reg_to_data(Register::A2, state); + vm.set_reg_u32(Register::A3, state.len() as u32); + vm.memory + .set_next_heap((addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32); + } } diff --git a/crates/os/src/syscalls.rs b/crates/os/src/syscalls.rs index 3ff42c8..0c1c964 100644 --- a/crates/os/src/syscalls.rs +++ b/crates/os/src/syscalls.rs @@ -12,7 +12,7 @@ use vm::registers::Register; use vm::sys_call::{ SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, - SyscallHandler, + SYSCALL_COMMIT_STATE, SyscallHandler, }; /// Represents different types of arguments that can be passed to system calls. @@ -89,6 +89,7 @@ impl SyscallHandler for DefaultSyscallHandler { SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), SYSCALL_TRANSFER => self.sys_transfer(args, memory, host, metering), SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), + SYSCALL_COMMIT_STATE => self.sys_commit_state(args, memory, metering), _ => { panic!("Unknown syscall: {}", call_id); } @@ -788,4 +789,24 @@ impl DefaultSyscallHandler { let bal = host.balance(addr); memory.alloc_on_heap(&bal.to_le_bytes()) } + + fn sys_commit_state( + &mut self, + args: [u32; 6], + memory: Memory, + metering: &mut dyn Metering, + ) -> u32 { + let ptr = args[0] as usize; + let len = args[1] as usize; + + if matches!( + metering.on_syscall_data(SYSCALL_COMMIT_STATE, len), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_COMMIT_STATE"); + } + + println!("commit state called (ptr=0x{:08x}, len={})", ptr, len); + 0 + } } diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml index 6f4e8d8..eeb8bf1 100644 --- a/crates/program/Cargo.toml +++ b/crates/program/Cargo.toml @@ -10,6 +10,7 @@ guest_kernel = [] [dependencies] types = { path = "../types" } +state = { path = "../state", default-features = false } [[bin]] name = "kernel" diff --git a/crates/examples/tests/common/config.rs b/crates/program/src/config.rs similarity index 100% rename from crates/examples/tests/common/config.rs rename to crates/program/src/config.rs diff --git a/crates/program/src/kernel.rs b/crates/program/src/kernel.rs index cd65de2..3a93dc1 100644 --- a/crates/program/src/kernel.rs +++ b/crates/program/src/kernel.rs @@ -1,31 +1,47 @@ #![no_std] #![no_main] +extern crate alloc; +use alloc::format; use core::mem::forget; use core::slice; -use program::{log, logf}; -use types::transaction::{Transaction, TransactionBundle}; +use program::{log, logf, Config}; +use state::{Account, State}; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; + +const SYSCALL_COMMIT_STATE: u32 = 11; /// Kernel entrypoint. Receives a pointer/length pair to an encoded `TransactionBundle` -/// (produced by the bootloader) and walks each transaction. +/// (produced by the bootloader) and walks each transaction. It also receives an +/// encoded state blob that it updates and commits back to the host. #[unsafe(no_mangle)] -pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) { +pub extern "C" fn _start( + bundle_ptr: *const u8, + bundle_len: usize, + state_ptr: *const u8, + state_len: usize, +) { // Copy args to locals before any syscalls (ecall clobbers a0). - let ptr = bundle_ptr; - let len = bundle_len; + let bundle_ptr = bundle_ptr; + let bundle_len = bundle_len; + let state_ptr = state_ptr; + let state_len = state_len; log!("kernel boot"); - logf!("bundle_len=%d", len as u32); + logf!("bundle_len=%d", bundle_len as u32); + + let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; + let encoded_state = unsafe { slice::from_raw_parts(state_ptr, state_len) }; - let encoded = unsafe { slice::from_raw_parts(ptr, len) }; + let mut state = State::decode(encoded_state).unwrap_or_else(State::new); - if let Some(bundle) = TransactionBundle::decode(encoded) { + if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { let count = bundle.transactions.len(); logf!("decoded tx count=%d", count as u32); for i in 0..count { logf!("processing tx %d/%d", (i + 1) as u32, count as u32); if let Some(tx) = bundle.transactions.get(i) { - execute_transaction(tx); + execute_transaction(&mut state, tx); } else { logf!("missing tx at index %d", i as u32); } @@ -35,17 +51,75 @@ pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) { } else { log!("bundle decode failed"); } + + let mut encoded_state = state.encode(); + let state_ptr = encoded_state.as_mut_ptr(); + let state_len = encoded_state.len() as u32; + forget(encoded_state); log!("finished bundle execution"); - halt(); + finish(state_ptr as u32, state_len); } -fn execute_transaction(_tx: &Transaction) { - log!("executing transaction"); +fn execute_transaction(state: &mut State, tx: &Transaction) { + match tx.tx_type { + TransactionType::CreateAccount => create_account(state, tx), + _ => log!("executing transaction"), + } +} + +fn create_account(state: &mut State, tx: &Transaction) { + let code_size = tx.data.len(); + let is_contract = code_size > 0; + + let msg = format!( + "Tx creating account at address {}. Is contract: {}. Code size: {} bytes.", + tx.to, is_contract, code_size + ); + let msg_ref: &str = msg.as_str(); + log!(msg_ref); + + let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + if code_size > max { + panic!( + "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", + code_size, max + ); + } + + if state.accounts.contains_key(&tx.to) { + log!("account already exists"); + return; + } + + let code = tx.data.clone(); + + state.accounts.insert( + tx.to, + Account { + nonce: 0, + balance: 0, + code, + is_contract, + storage: Default::default(), + }, + ); + log!("account created"); } #[inline(never)] -fn halt() -> ! { - // Signal completion to the host by triggering a trap and stop execution. +fn finish(state_ptr: u32, state_len: u32) -> ! { + // Persist state back to the host, then halt. + #[cfg(target_arch = "riscv32")] + unsafe { + core::arch::asm!( + "li a7, {commit}", + "ecall", + in("a1") state_ptr, + in("a2") state_len, + commit = const SYSCALL_COMMIT_STATE, + ); + } + unsafe { core::arch::asm!("ebreak") }; loop {} } diff --git a/crates/program/src/lib.rs b/crates/program/src/lib.rs index 875e1d1..509ae85 100644 --- a/crates/program/src/lib.rs +++ b/crates/program/src/lib.rs @@ -16,6 +16,9 @@ pub extern crate types; pub mod integers; pub use integers::*; +pub mod config; +pub use config::Config; + pub mod transfer; pub use transfer::transfer; pub use transfer::balance; diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index bb0f501..aab4ad1 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -5,8 +5,7 @@ authors = ["Alon Muroch "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -hex = "0.4" -storage = { path = "../storage" } # adjust path as needed -types = { path = "../types" } # adjust path as needed \ No newline at end of file +hex = { version = "0.4", default-features = false, features = ["alloc"] } +types = { path = "../types" } # adjust path as needed +storage = { path = "../storage", optional = true } # adjust path as needed diff --git a/crates/state/src/account.rs b/crates/state/src/account.rs index 2da4603..4f290a5 100644 --- a/crates/state/src/account.rs +++ b/crates/state/src/account.rs @@ -1,4 +1,6 @@ use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; #[derive(Clone, Debug)] pub struct Account { @@ -8,4 +10,4 @@ pub struct Account { pub is_contract: bool, pub storage: BTreeMap>, -} \ No newline at end of file +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index f144646..8930e51 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,3 +1,9 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; +#[cfg(feature = "std")] +extern crate std; + pub mod types; pub mod account; pub mod state; @@ -5,5 +11,3 @@ pub mod state; pub use types::*; pub use account::*; pub use state::*; - -extern crate alloc; \ No newline at end of file diff --git a/crates/state/src/state.rs b/crates/state/src/state.rs index b44ef30..a48a430 100644 --- a/crates/state/src/state.rs +++ b/crates/state/src/state.rs @@ -1,10 +1,15 @@ +use alloc::collections::BTreeMap; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +#[cfg(feature = "std")] use std::rc::Rc; -use std::collections::HashMap; +#[cfg(feature = "std")] use storage::Storage; -use crate::{Account}; +use crate::Account; use types::address::Address; +#[cfg(feature = "std")] use hex::encode as hex_encode; -use alloc::collections::BTreeMap; /// Represents the global state of the blockchain virtual machine. /// @@ -47,7 +52,7 @@ pub struct State { /// EDUCATIONAL: This is the core data structure that represents the /// entire blockchain state. Each entry contains an account with its /// balance, code, storage, and other metadata. - pub accounts: HashMap, + pub accounts: BTreeMap, } impl State { @@ -60,7 +65,7 @@ impl State { /// USAGE: Typically called when starting a new blockchain or when /// resetting the state for testing purposes. pub fn new() -> Self { - Self { accounts: HashMap::new() } + Self { accounts: BTreeMap::new() } } /// Constructs a State from an existing Storage instance. @@ -72,8 +77,9 @@ impl State { /// NOTE: This is currently a placeholder implementation that always /// returns an empty state. In a real system, this would deserialize /// the state from the provided storage. + #[cfg(feature = "std")] pub fn new_from_storage(_storage: Rc) -> Self { - Self { accounts: HashMap::new() } + Self { accounts: BTreeMap::new() } } /// Retrieves an account by address (immutable reference). @@ -154,6 +160,118 @@ impl State { return true; } + /// Encode state into a byte buffer for guest consumption. + pub fn encode(&self) -> alloc::vec::Vec { + let mut out = alloc::vec::Vec::new(); + out.extend_from_slice(&(self.accounts.len() as u32).to_le_bytes()); + + for (addr, acc) in &self.accounts { + out.extend_from_slice(&addr.0); + out.extend_from_slice(&acc.balance.to_le_bytes()); + out.extend_from_slice(&acc.nonce.to_le_bytes()); + out.push(acc.is_contract as u8); + out.extend_from_slice(&(acc.code.len() as u32).to_le_bytes()); + out.extend_from_slice(&acc.code); + + out.extend_from_slice(&(acc.storage.len() as u32).to_le_bytes()); + for (k, v) in &acc.storage { + out.extend_from_slice(&(k.len() as u32).to_le_bytes()); + out.extend_from_slice(k.as_bytes()); + out.extend_from_slice(&(v.len() as u32).to_le_bytes()); + out.extend_from_slice(v); + } + } + + out + } + + /// Decode state produced by `encode`. + pub fn decode(bytes: &[u8]) -> Option { + let mut cursor = 0usize; + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > bytes.len() { + return None; + } + let slice = &bytes[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let count = { + let raw = read(4)?; + let mut buf = [0u8; 4]; + buf.copy_from_slice(raw); + u32::from_le_bytes(buf) as usize + }; + + let mut accounts = BTreeMap::new(); + for _ in 0..count { + let mut addr = [0u8; 20]; + addr.copy_from_slice(read(20)?); + + let balance = { + let mut buf = [0u8; 16]; + buf.copy_from_slice(read(16)?); + u128::from_le_bytes(buf) + }; + + let nonce = { + let mut buf = [0u8; 8]; + buf.copy_from_slice(read(8)?); + u64::from_le_bytes(buf) + }; + + let is_contract = read(1)?.first().copied()? != 0; + + let code_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let code = read(code_len)?.to_vec(); + + let storage_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let mut storage = BTreeMap::new(); + for _ in 0..storage_len { + let key_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let key = { + let raw = read(key_len)?; + core::str::from_utf8(raw).ok()?.to_string() + }; + + let val_len = { + let mut buf = [0u8; 4]; + buf.copy_from_slice(read(4)?); + u32::from_le_bytes(buf) as usize + }; + let val = read(val_len)?.to_vec(); + + storage.insert(key, val); + } + + accounts.insert( + Address(addr), + Account { + nonce, + balance, + code, + is_contract, + storage, + }, + ); + } + + Some(Self { accounts }) + } + /// Deploys a contract to a specific address. /// /// EDUCATIONAL PURPOSE: This demonstrates smart contract deployment. @@ -201,6 +319,7 @@ impl State { /// - Storage contents /// /// USAGE: Useful for debugging, testing, and educational demonstrations. + #[cfg(feature = "std")] pub fn pretty_print(&self) { println!("--- State Dump ---"); for (addr, acc) in &self.accounts { @@ -246,6 +365,7 @@ impl State { /// /// Storage keys are formatted as: "domain:key" /// where domain is like "P" or "Balances" and key is hex-encoded + #[cfg(feature = "std")] fn parse_domain_key(key: &str) -> Option<(String, String)> { // Find the first colon to separate domain and key if let Some(colon_pos) = key.find(':') { diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 2307694..6410daf 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,11 +1,14 @@ -// crates/storage/src/lib.rs +#![no_std] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; -use core::cell::RefCell; use alloc::collections::BTreeMap; -use alloc::vec::Vec; +use alloc::format; use alloc::string::String; +use alloc::vec::Vec; +use core::cell::RefCell; /// Represents persistent storage for the blockchain virtual machine. /// @@ -159,13 +162,14 @@ impl Storage { /// /// OUTPUT FORMAT: Shows each key-value pair in storage, with the value /// displayed in hexadecimal format. + #[cfg(feature = "std")] pub fn dump(&self) { - println!("--- Storage Dump ---"); + std::println!("--- Storage Dump ---"); for (key, value) in self.map.borrow().iter() { let key_str = key; let value_hex: Vec = value.iter().map(|b| format!("{:02x}", b)).collect(); - println!("Key: {:<20} | Value ({} bytes): {}", key_str, value.len(), value_hex.join(" ")); + std::println!("Key: {:<20} | Value ({} bytes): {}", key_str, value.len(), value_hex.join(" ")); } - println!("--------------------"); + std::println!("--------------------"); } } diff --git a/crates/types/src/address.rs b/crates/types/src/address.rs index 8dc9be3..c7e2760 100644 --- a/crates/types/src/address.rs +++ b/crates/types/src/address.rs @@ -4,7 +4,7 @@ use crate::SerializeField; pub const ADDRESS_LEN: usize = 20; -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(C)] pub struct Address(pub [u8; 20]); diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index b4ca712..ed913f1 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -14,6 +14,7 @@ pub const SYSCALL_ALLOC: u32 = 7; pub const SYSCALL_DEALLOC: u32 = 8; pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; +pub const SYSCALL_COMMIT_STATE: u32 = 11; /// Trait implemented by syscall handlers consumed by the VM. pub trait SyscallHandler: std::fmt::Debug { From f5b93857a345ca81fdd51ec1947a891669b53855 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 21 Dec 2025 08:45:26 +0200 Subject: [PATCH 13/70] Rename os crate to bootloader --- Cargo.toml | 2 +- Makefile | 6 ++-- crates/avm/Cargo.toml | 2 +- crates/{os => bootloader}/Cargo.toml | 0 crates/{os => bootloader}/README.md | 0 crates/bootloader/bin/kernel.abi.json | 7 ++++ crates/bootloader/bin/kernel.elf | Bin 0 -> 54872 bytes crates/bootloader/bin/kernel_abi.rs | 30 ++++++++++++++++++ crates/{os => bootloader}/src/bootloader.rs | 0 crates/{os => bootloader}/src/lib.rs | 0 .../src/memory/memory_page.rs | 0 crates/{os => bootloader}/src/memory/mod.rs | 0 .../src/memory/stacked_memory.rs | 0 crates/{os => bootloader}/src/syscalls.rs | 0 .../tests/allocator_test.rs | 0 .../tests/memory_page_offset.rs | 0 crates/examples/Cargo.toml | 2 +- crates/examples/tests/common/test_runner.rs | 2 +- 18 files changed, 44 insertions(+), 7 deletions(-) rename crates/{os => bootloader}/Cargo.toml (100%) rename crates/{os => bootloader}/README.md (100%) create mode 100644 crates/bootloader/bin/kernel.abi.json create mode 100755 crates/bootloader/bin/kernel.elf create mode 100644 crates/bootloader/bin/kernel_abi.rs rename crates/{os => bootloader}/src/bootloader.rs (100%) rename crates/{os => bootloader}/src/lib.rs (100%) rename crates/{os => bootloader}/src/memory/memory_page.rs (100%) rename crates/{os => bootloader}/src/memory/mod.rs (100%) rename crates/{os => bootloader}/src/memory/stacked_memory.rs (100%) rename crates/{os => bootloader}/src/syscalls.rs (100%) rename crates/{os => bootloader}/tests/allocator_test.rs (100%) rename crates/{os => bootloader}/tests/memory_page_offset.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index d39f2a1..48e828e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "crates/avm", "crates/compiler", "crates/examples", - "crates/os", + "crates/bootloader", "crates/program", "crates/state", "crates/storage", diff --git a/Makefile b/Makefile index 2f6683c..bd9502d 100644 --- a/Makefile +++ b/Makefile @@ -12,10 +12,10 @@ run_examples: @echo "=== Building example programs ===" RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples @echo "=== Building kernel ELF ===" - @mkdir -p crates/os/bin - @$(AVM32) all --bin kernel --manifest-path crates/program/Cargo.toml --features guest_kernel --out-dir crates/os/bin + @mkdir -p crates/bootloader/bin + @$(AVM32) all --bin kernel --manifest-path crates/program/Cargo.toml --features guest_kernel --out-dir crates/bootloader/bin @echo "=== Running example crate tests ===" - cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../os/bin/kernel.elf" cargo test test_examples -- --nocapture + cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../bootloader/bin/kernel.elf" cargo test test_examples -- --nocapture @echo "=== Example programs build and tests complete ===" clean: diff --git a/crates/avm/Cargo.toml b/crates/avm/Cargo.toml index 48d0eae..b939a92 100644 --- a/crates/avm/Cargo.toml +++ b/crates/avm/Cargo.toml @@ -10,4 +10,4 @@ state = { path = "../state" } # adjust path as needed storage = { path = "../storage" } # adjust path as needed vm = { path = "../vm" } # adjust path as needed types = { path = "../types" } # adjust path as needed -bootloader = { path = "../os" } +bootloader = { path = "../bootloader" } diff --git a/crates/os/Cargo.toml b/crates/bootloader/Cargo.toml similarity index 100% rename from crates/os/Cargo.toml rename to crates/bootloader/Cargo.toml diff --git a/crates/os/README.md b/crates/bootloader/README.md similarity index 100% rename from crates/os/README.md rename to crates/bootloader/README.md diff --git a/crates/bootloader/bin/kernel.abi.json b/crates/bootloader/bin/kernel.abi.json new file mode 100644 index 0000000..d2293e3 --- /dev/null +++ b/crates/bootloader/bin/kernel.abi.json @@ -0,0 +1,7 @@ +{ + "version": "1.0", + "functions": [ + ], + "events": [ + ] +} diff --git a/crates/bootloader/bin/kernel.elf b/crates/bootloader/bin/kernel.elf new file mode 100755 index 0000000000000000000000000000000000000000..8219a792344803cc2004ee8a2bb439cdc2942c7b GIT binary patch literal 54872 zcmeFa3w%`7x$wRA?AepaWO9K`5prOxA)$&gai$pW zJYSuz!1YL``}Oj_{L1>vsh_@oL6Z>jtEHY_d~neW{ypjAJL%5<-1r{|{0{{F2LfLX z0+%icnG5QsFPc4fR%p6u&RaZpQD{1UGiEHPTevV(8Je^(WX_wrXu))I(N`!=oHwH` zv~c!~bzcdEzWe<}bqg!IYR#dY8Q%}pEt$P=(Zcjyr}4TOfBfmn?y8}(;k;$m)y-HK zns`x7?Uk2Ix}x^V3nqPI(xss?x7FbfARKd<$_}olaeaYn2G_bJb>?Dcv2kMZ z#;B4RZk3Sxa%krC*>mb<%$z-U_QI>FntGV(cHUfb`uyqU>_y)X&AXv)!OS`HzQ>s6 zBdq&zT`~(@z?Gqj+xS0pwGh*|)-G5uZvpqxA1U*v%@$&MXy&{H*G*rPp3dw=q1o5X zpHp{T-P}cBWZvA+g1SYEnc9rd^tqur*-GepSJ%xAExNic#60U3%=x~Y$ihWH{<_eN z*)u|O=Pl|zjwSTtzs4QMb#>RxTk!qR^f`0pnQr^@W~Q*1nzj%XD3*N27vq}Eb>W=Z zW?g7Doh_lC=?fMul;i8JfrYbml%`)*-iD zZh@&;i>?l>rfz@QXysbO)qRsLQZ0-gGd5CHJ#PGjGpB#ooKZLP72eD1@&3%|a~9Su zTCfBiA1U*)sb)tS|!tH)H2t&UVzRaaM!s~%rHq58~mqsNUI zH+EcPT-CVhapT5~A2(s#nd3)~A2WXJ_{jLG@zvwUjUPXL!uT^MjGi!N!q^Fs2~`uS zCybjge!_$aXP(Ir&!qb^Y5Gj6ohffEnYFlX;iAy|>2qhB(-$tRTj21M%iLcH&7QF& zbXI6$Y;tVkq)Wef^7FbGv+63ZuA4pU>P4ZieJ!-GZqCefab#%B$?q>*U{*rN$Ti-c zv2c-FIQhL(wL2FP)uVBcA?1VoNZNHj-T#Gz_>KA8mxr7-Lk98D&(&Mj7Q7k21=pk21p7kK*|#o{us@>qi-t50A80_=Fzt z3A;fPdZs3JBG2r^l*zj9=fZ~1boj{jN1eWOEtaGE+A?h3S$mrh@5I~gHbS@OxOKFP zb>H*UQF8Q*XpcHkVIJP(u@NXbU!zPZFe;P+UE}_H8@%>R^QHEUemqP|q$G*4bYX zdVWGg>b48Lz^B-28kNYQJt8urRhW6+pln07Wt6QiR7+ynM)`W$d$`bP@9+knJC^E8 zbzf9DdHhO_tto|T<8tg`1@&{R()!f+7f;cBx1ChSPGpF+z?ZqctJV%;oSBSsIO9C? zl;hl4^0<ZjcRxvO1{M5Bq$@=k)X&Bg9=LtOsjI_2B;}h|?u4#Ve6V$al zp=U*b`T2@@`zDmnq423i&=Dl1=-CdE}c4{m(Fuye8X-nJ;HBzuV4>WV|OLzNbV|dqi00J}%VS z{Q~;(skQG2qgY-)5k?W$iOYDlRv2MY!6=K(K?!{}sPYwI-M-Yj7@ zDjB#!QFlY{yP@|z(EFaxp?7E~%`4^rpO}}|2G8UHgNL)>IgN38Z0#HgSFcM0ZRa}u z%y>YKZ&XBkk9p@qr|`8O_?=TCtaGa2sYVf*xlibY84i9U@4O?Da~=o3kEZY&tuc>0 zLL2c4iBGdJSZpH$^u~3a`m$<~-0_UC8$a%he7r{_eYeH*?>*ib$=Df9uF6uZA|)Gs z7WT42+FCm-vizt}{eEFy==VnapG5T~6GTUU{|WQR;~x1e>F0TwUsw+=7m;`UN@U69 z!YuGpf0^ib>+uujv9;L||KVtI*=NF9o*~TN`2)KDSf^~i;|^cvJP{qJFFBUG(~nG8 zer!>4xnHrKpuby|Yqn=J*UOoENSFe(KV zV|p(4*`DWf%%NQGSgz==ERWg#!+$s5@Mi*(vEB9B#`LI{Yvx?;u|3gTGw*VLk9x<< z*JM5Lo*`l#@2>qDvN~Ho=FhSHB4Bz4YVi;e+RHS6!ZoPlV?@4h>fz>l%Y8UTJ&MZahy|BV$2VMzsy@=9g%oFTbdd_vRP9 zaG4UB`M8M8YDES~86{<8yue(vPhK;ul6uv=6gd^0Jl!a3_nHE}*81>u_<+Y9V{`(t z#Tlc+*A?qM$QY0AOY^n(ja9A`nvH>h=u!<`>P63H8^yN=)#6y8eRBYw9iytRzn5ov zg3K{X*jd@=Rrn{rLfH8ih~%n`Q9Bczo4rR^#XJKJ=3m!&^hU3+N}lahOX{;zGNi~H zqIn{Tba%Ts+_e|-hYNksBoWazMI(t#LJcSUR>^Mv_zV$G z&O9Wn(stl`)Nhs6t47$ldWWO@zmMWSWZdW^gvire#;%lkwVT7jUUN3^DN!PgJEBGrGCdJU z>kv&(-0V*#8lp*UyYmh(_PDgsrmAu-_iYx8G26I*voJQ?r6d~~qIz(MNH*Nfede`c zgP-TB(dt}1#)BLD#@d_voeI9-@&k0BYLs29oEoO_3-t_iC~!0iwY)T5bZxRQZ`$Oy z%3~gD?|XT6Vtc1ixHC`P*p#KVK9yy)27=ZD=cz{M1CRP(lWINi6#uW!VXiqw@e4Wd zIvO!>eI$)Yjk`ctrQ6c`@2(l-KiagWpb^2d{b4@}xoc zMkQV6o1RTU+fQ5CRmh=!irIft2tM^#Q{HBr8{Ki1)CARVL(mHE3R!HOz%}5G_rdeL)&otS zH{P?l#_GWJsn3V2JKp)-a1Hbrb9A2Wo!ooOYn$M!9^+1&hm3>%q4#9NHt4olm`hf8 z^_9Dz`QlhaODOu96wR;L6V-j0@ZiH~dfzAEBdFL%~r!T*;cqA+bZ3a!oOFuN^7*w!GCEl z{Ab?TsrwqVf1L*6-*RbSS5J6e(G&l+4;n3exfeXum*30NyWqJY%G?zRADf_odDE%k znrs5DjRB#r@I&)y*tWIl+KQ85%X=w#@_(R1#rCtdIVF7W_&4CYJkKgsJj|z%#%m%jWoyUM2R8HmVBR0h`-3fHo3$Bxp=|w7#y!-iXdf!qZ)Q`V zMrXaLFT<{DU_BMREoh-%spnB&-JNG6f90|L6r$UfYs?;A8WKU-G|{1p=Lq zoC^FB`{XKY%qNH1IP0K?p|%DMhAu8KilF1-hl_R1StAy^Yc??u9h9pFS@&J`cAl=) z2z?;*oiFKIizNf#Vqk?ZZ~S{u>h?%uDRMnOX6K)c9tdNbw|k^)Pp(APBiE&_H;O;- zpqCWZ>Z%KS@c?k}q-41wWjS=L7#lV_vQ73O+ep_>;j4u@!+^msU^h(GHST`|UU-T+ z-hS|jN9qh@hZ51ID7uzF_oj7+xX&tU_gJO%o-|&hK9)EE2dWNj0T*=NsmIi_-=udn zeWFuZxA~0+cXjFQUhO3cStAWl^u|(0rWp_J%`&DZlafg6|%EZUfXl6G6nvD=8D>( zyB7vCkJIhoot?In|DTKFw!h1IdczQ*zW#2Wo;X+NcVeqGW2-fkxHv8>#;#nMZRgiu zW3G!yn=Z2IXtWE@#S+gOd*c~>V}w6Q>+9v856_sykKe`fM#ZtgpyxjLZs-a3p7;h1 z)YrOjE@{QZcYUgFQEy>qx$-ZL-S7qQ+HglNyr%I94XHNrSl5PN4<3WJD&W^A^Nh6* zg41o-s;}@&;xryh;WSUmWg}FefK&O*!RZ&nXML_wv>ut#p4$_rX_|#EyKF;q{HQ@BmY=m0Pi-V?0~oL`cA?>l68%u8j4TXEumJ{`}CnXid7l&+4tO{SjFu=dvybI zi)HBf8gNoF?B;~R{4=D_#R@BadyP*4{~2=4YL&+P_}B1%wEOje_$|un9lwEvkIVka zQeheOnYOJe>Zn+zKI~S-8r7a@-+QaTehKIUyox$p3Fw1XC{}rWAo-Qa!0qBJ3!hfR zyhMro^>>V`J&^qC%Ze3J3e}2OA@3DN4sI05x;6MG_k)-AtjHnO&F|26X?<4moexFi z>K_Z_L{{?MVM_A5D}*&MOHnU6O|dQ-p{O-qRjit)g*xf?!aD!YLjA@+M8uqn9Xve{ zsoSd<gq^H$9G>$9U!(6OlvxvGLmj%!&Iy#xHd~ zeyMNax4RbK;SYp;Z?2*bXi%7&uGs^Yzr=j;cOCL8$yskIk*km3TmG#`Ui~BCJU?^@_#BgC z9f)tdzzDrC&?s6z&?xRV2w&kqqr85QQE~AgGbek1Iy{!Gds)8+>rb;TeW=P@>h8>$rZiSK2O$ zS-_fB#67Zo)0bJKpEg27yfYNF*jue<{=L(>bVF6gPrS$EdR)){s?blnRwM&kSbJUy z?JI+IUtD$8m9BmNFYrvjwKtS3*WQR_Nqa*rsn0T69s488co3Un^IhsR!B%Z2^Fpn*%Eg-ezT{f%Xc{YHkOzqw4=`jnQ8hr)(GyR^ms!?2NgU0C1S zC|WYV9X0}=gL=Y#C!%J>j$-DJzHu7{}OHq{xoc4uL`&1jx24-{;br< z`9ruRhqiKm9R{ZOrXONnKUIv1XOaD%p@Xu~-N3x)As6Pw@(Rqu4|(iWr^_+-@|nyN zlP_a_h1LjQSs5E>jjA80ese>WI;wr3F4`*l(m)FTwK8U96l-B4U#oJ~&7lKXM#UMK_z}>f z`0mD78I>=T8KDE^=yiN5^&^luo)lj#v_{mRHyciC%}S7;crshNJU_!?(lLQl;E*d?*j&>uJw1!uWeK9pB7k8-J)?fQ{{&9V6bt z+H?nPhDC&Ri`|S*tZ}zv0~EOP@=hm#)T~ z`Rke2NcZ431`0F!*|CJniC6rFVt-HT7NT>3PT*%i{yiOjBa z{GEfO@7M6}^%$Y;9@<8ZIM*P)PmdAa?J@F;tC7F`&ES?HYDImfu00cmzWk2wEPQ>*kD~vq zmhRHk67K-3q-Fp8Yzen_At*At@X2IkW z$>DHm@W*zXwO~(upBohR)Rl^USx~VrTPakz2Duzve|au)l{H93d!~NLO6;njqJL+l zr0IbDMuTF+k##+70PF{2DZ0I<4Y0QXS&Dw|VFNJ7%t+(YQKPY`mmIYVTwB&yeit$n z{}#4D%Zk^Oq_&H7VM2}gejjz`(6+#iqvJ=B127^MfZhW==cD(T5B5-IPdldXeEQll za(=QsV=3eAH5caB*Pg*%>N^j2{N47=)$dwo2>kyDHU*FC$OiMxGw2{NnQ4I|_Sh6!z38?5R;- zTz+55`!%DmyGCJmjrxztZ}_vf{O(XoYseXDD8vpb#13L@QN!Bq^X#B#sg&P(*4_9I z8%BM;3~&1b&j+4bhKtv~kSvh?$Am|C#pv?s~A@pIQ%Qy6Zu5VVw1#5|Hb`Wa93qp5n=*_GpR+pHA^${43b$a!vAu z+QPOW#!??p%i6JRuycyAa}4Yp13L#>B|={oc8nU14Z*YvimgY83OHAJ)RR znfPiJeHYXBu3b-B52f#+>hK0>dlg~Z7h&5MVcQqU7>Cs5rZ@9EJAj{| zA;|yVQpmshf%>gYRo1iy)<5_nXKuONZW@d~jCjW}U{-lM^OZJt6hAumnzrJ%y-JvC z9vv!k>A=}g=N)o4jIuL^8|4=dH!9YbF;46WW%%03GKLx5sulyU6+?vnQ`a}HPTN3# zyTaJA=oxJG!Nf&YpN0Qpjxax!zH(wD!F91@g80K4Vk3@SUxB~q;XuT06##L#67Vre5>d^t(I}7{Obe4_WKOr-Fv!FzqtjxJ{{JFj0E=IfaeoRGAQv&3}pVuTp0tA^pf;$ zO5v86h=beWZrnmkF6^Rz@4{`@y|yMj|7$`&{W{vOf<_(!&og8j;J6AL&%mB&&q@aG z1{YhfnZa>a-RKbq$CSI_+?F^soZkn|TY>YV!1)>A{JNx#6pj;paNPD+;N;*K|6Z1Z zW7(%e`)xl?k2e{32wZIH$+))4>$B`&7Q9TZ6!{{NmD9lYaB#D( z%4ptNC2=O{!HK8bJcn_f&zM6oC!R8(vC4_36t5}m8BdYCDRD04lRBcE^}j1$fPJnz zhxkIZw7oyMxw^WM9G#743$+!Wbn6~)hW@L>4-jgvFp6H_dmzvu*8U4^6jR@oQ?3k{ zhio5Vm9&q*UaPRev5Hg5DdbYefXp4{fNb(`CRgLPnb6aBXCZ&dH^P4H_S+a`8>@|C zd|v~yq4`+0>D@xz=S$SRo2z7e*POAPa$eXTP(n@;@s<2q{MSl}(Oh1gOf*BI*oHsb z9ah8b#l}{7CH}lwUSq{}LA5X*l>Z}R^6Q|py-~)VZ8sLMR(>I9wC)Z^8t_*+vcn5q zrSTrUsxMBBqD87vyaBnf4Y{$)lWe#Y-SaZEwYu8aFs;;BjgG+=r>`Q0dU)79a{VCV ziuxK?W+KZ2#A=}ZV#m)G3Emx(ykvelCqwd;UQi)@1JFJE=ERd-A4p0^?E)XJoRq#~ z2Pa~xj$E{x=TNtbyrUViepD|!3z@kGT|Eb%-Y=Q+Civ)3N>7V6>_BL)Z(Gre({0H% zl1{9=hfH((yQnWrR4GqSA=7McX0>^Ej+#ttz=vF)6-jJ>-V@FoQ}fxWYxo|u5yV+} zhj@f>-!AN+O;zTSZPJd&lH+#e-Bo?)Kir%CrJOtbW~1Faj&=4-=8O;XHVu9k@L62y zK2`n??FQG5c5<XFL4=W}hHc$$q5ocUy6@1nE2)^+!% z>R&?qlvwH+x!q%mcejNM)(1%WiysQUQ)_Fy9sQe+S>;}@twsGt(Zz+4>d8urw!WZe zE_5o-Zh`Yoh`c2ye?lpia>Kyq$J`|DrgNWIeLa4r_4xl3DVw0-!|A!zq~QH$(<}4# z#1uQ|m9&|ocgx>F?z8A6=yBRY*0t_tzK4Z7zoWJ9#KE0g|6*Nxn4CW)$8A?!1Kl_Pcf4&hK4!eyZ;FbGqxsC#l2Tycfn-j7$x7;SB$1 z8gFVztYBw}itS1a`3O00p|%M|<@#(hkR7y!$Fh;#+19tVRGINCpR~>NyYS05v%U}1 zDtZu|ENvu5_jT#lc;8r(IkcJFQe;rzsS|qe0;QuMa9rjTV8e9Bk!ahDXZrNhM`BBD zQPdGx)%FVFNc2tI$&DlVu1evkS}OiPlfg_@uLANX7~IuJg_fJN-7#bn4j6Wvm-A{1NRJ@Lg>`wS+#G zeL~)w*01BPj1#()O?RA&?)gPD;{OG@`CQt?&L|-Uml~TIUheM*_&&Buo}gVfKX~5Z zD?iqKVu0MwWL>^HFMZ#d@@}5YG1oQnHU9XEV``a3yw5M?L$KqHjN{blhrDNx#X#qs zzI}HL@JaZS*W*>l<;3sa0NhUCioWf4f8GITh6zFZz$W-lKW7&J7!3|+-Ji^(YK+KRnQ4n z{|{BkhsYcBO1|V7_NS~bfF3f_bOZkVeQ_^sOf$#lmHaMo!u#+q`02tSw1C|tpP61C z`qd{oZu6ax_&C)&9eKVJ&?7V>24Hi@Yd={x{cg7mPfW7==(tnL7~~5ya~pE95MSRw zbrf;U%KCw7c`By)qlfYw9&ctX+B^xqBi~`=R$(6BG*VAopd_`g#n6R8Cr_ujHWgzE z0@oL^jPe80FPfFI`A6zgZdU9mS;QxACPzcbvZoSDzKmG%Wy^^p#j@;Ih$B_@;|hZMHMOb=U~Uh;3h=g?-RFmL9+-;L{G<*g)wxY8oEq;Yq{wrd(mM*0)za+G#Z3 z{a+_`+IQVke4qlKzO+xsmmqfP!tOq`q(*e;B|f19cBlLNPHd&#Ypl}x3i>BszB`6$XF2|ynw~Uj z@4J;4J^uOqw~C&=`kwL6Q_vzlZRo2fElz=E8=$8YEw&`^@lS$JFG7n(eoZQd<&KjzNUqy$h(fb_O>8WOU$Ey)L_JB%0gZ&-yudd8c z$ipHpQtHh{ax`#Gz0oMpT#(L%@4n?JGy+uS7SyweXYTF zR!n|E*~5NE=R0vubb8qb_<5B~Bh-&vpo@J*#dM!he!Y);mkieP8AkDe45R3S3?~li z$lmzRT=}?F`YSRGbhTR2o+))Uu*fo&I@dspbH(RjZAEwA#JlL#2Kg-JYjLjiJ|p4O z^|d(H`YfZ-shiQ_Tw@u=GFh(AXyGd5DE(weJx)JBkKL=df(^G16eQo=re~Q*dEv+4aXHDf!t5Lt|040Pn*M) z_1GpWhG1u)4-%*0V{vT_<38m0{d_m@ZRPtQ-_3lt^1ToFem`=31MHdExMWY6-ZlsuqtI||jNY-y&z0%Xn7u;o zYmjltQ_1x>YbqI&Je6E;>;FaMgQIV|Vv>DjdtCZqdgXfcl@IFgi6iv1!-EeIJ33S7 zO~hyJhL4ZUCN7Chp83_7*|mAV)%ksqQe*DsXBZ?o~pdUQ?2saPF!*;>&`8- zk>-y_T>Dq@hpXeRm;SUb+LlfZZiMzC+U751yI3;s)lV)}{xy#M;@Cq$;zjtoZ0XOE zu|`)uf6Dm()!YtzXK@QV-;8hBKz$jj#Anu3pEa~XoH9n7vc60$Z7;(%4YRHxUP-L7 z_yzox>~$bU*x}KN&=u_6VtqYrW0m!4!if+1KmN%|`MKz-}TeYkI3xuvI{U%%k9 zPCaW^*gnnm$=EUuRB)ZJrf#T;Xp4z6?T9)tpOxg=HaAsUwOZ;7gA($@tH~3`?|<$T z^2F!`%pY*hvG~bSnHw%--g{)$F>)^Z zvuFG<+S*N9dui(+^^bPvYTiM;pLm>n@+5hh@@#_S)tB&IzM@Xl6!pRqMXjw?WS)NV zkL=NB9{^{TD3Q9|ip+tRIp&c=k2!hfk-919BJy_15A?{>Twk6Ht_F^7zLpc;a%uHd z^??gK__;jRM)o$e234!|0_@}7x>v3>I+kp`$tXJz&;$7T5>JI??7+@1CkAjH{6Kt_ z+)5c|N^0ca%k_ub{!4m3xtoKp;r?dgZpa5GMnE1SYY`d0O!40Hefe8GMJ}hb)fodm z=N@@yPCxNy`qH@i`^{CzxUL)~IS2G`nN19!SDfGa z7IExVgM__yxX{;568hb&#VbAtL^3{M9|1AYWkGC@_rc-%Uh#PSE@aq=?`KQjViH>< zqG`mrrO)93p|2YbJh5r;)#CHcO2L%5%6OXV!%gAik!0|in2ayl`PWHXCn{|nY3E4# z=!$XEj{AO8#((hJ1>v{)PzoN~nN#olfl?)C-2eVb@lT8RlkUX1$q^kc;av?4Ch*I2 zwa*?YViedAJ;!PX{b~C7-x0TjU$}g7vwX04mMUvxoP`C*n!xItc#I@S>}z;UJ%jVh7M%x`{z+N7TQ`Z*PCwa zdn5CZvG08+SZ|ysb)%kr)#u2h+&}h}Nj5S`Kc!59o}^4l5QqM6E0fx8{lYToM_b3* z&Y2fk@XLXKy)qLY`su=6fqas(DgQi4<4z1PnP|l?pIVP5$encM(~2)BpBkaHUiQbA zlut{)qT^@5ZRS@{Ukz@?4&%ThK-xu7GHkx?1W9D2%B7ydKSdV z=OqgIjm7v1?9>;q+gDX?WPZ2K^STnzSyhmFk#!^YO5 zeeyEUcP@UxPI57jOHF6RpdUqtXCx0fvdi%;blFH-fO$%vbI--!*L3OsY@Q9zoR#94 zIlXzNYtP41(Ui=4AeE2Nr>*FObXyJ`2k`56=a{8nA;$HE1#%GGe{$ZD9`II6zA=y7 zsbc$qhZ~iMZ+y(n$>2&O*f}#{V7LJyR`nOCmCF%uzo`y zzm?L*jn&TneJ2+!0q?r{Skmjfw{G39tHfdhQDFz7URyg%>@Y;1@lM@4SeS%t4@cN+c9VN5rX=joOEa~1tG{Cw=uB?(2bf4Yh@8t-IZ z+gFuj$3pfXKg53Fm5gy6V`Qy<{-eUaO*reM^uAo#x7_C&smmA1xo;}TKYpl0=G{p@ z&oIB&srx?h{o};YGi6_j++VE^VsF>oH!G2A>F00k3x0cqqD~s3Sd)Ulph8hEILmEw z_L*|;^Dt)*@sO2DWX?^j`KBsed+weaHdlH!Kf~Vhok^RVjRw5nrZB{7O?~rrPOcCmnbwYi5F+PKKVl^+Y-|YkP zT@K_JWlFC49Q65YzObHwMt4>7&U*4?`jxO}yVwX%FE+}qFIJ8EY?&KqdiDMimMZ=P zcOQ8yK;FHavkM2gH+slQ2M!V@?2i`qx}b!O!jm+zSEZPCxV9HlUzSUBRVN=HmSx1* zBmSWCEQ_{ISkkw;b;k+XIRQTowVRr}Qf64`{jB6{^^_MK1M40oE*gD zK1Pnxhf|LJ#$1}=)l>Nn=@|Ihkdte)NuDDi-*xg2-S;0IpO4JcYS-FY+>>haIqLU{ zp{MF^?5ZzqI=B8WoUu6Vw=H$^qRHoI#MXA7@6e^ZBZl97Pi&Vl{T}6QdP3TXJ?pjo zi_9Bwb9K^f#UGaWb287TyKeO7QeHY|?RE7j{~b>{7ykLBEp zZ*6TU0w$>C}SKF{<3Kox~?)`*&DOihN z%5`>+{#u;2G}%^q4B~!gj?BHg&$j!VarJ^@{9bwPp-nzriryx={q4Tf)+VRMApWHr zhu9Mi9K^cxy;S+`6uuiB+9prH$-|OxPUmPq?`DBYZjmy~T#XOzzG1iOYpStH>fC$> z4Sc5aZ|=Ktm9445_U%^fW^`u(GGD`gwQL9Wb(^GrZ^z0*pQ^2UI?3HRVQmD*>AHun zl>Dxm=t;>__#@TyGw7}`HO`ML&J{!sXzqLMoVC`jrQdUMgzvi*KMlF>0epxnyu!RI zTgr)S@;Ce2GN(_pafJ`$xgo=2(z%@BS$L#!4 z>^b~uiP~z5Sck41?X(l#>W;zM30>xyonkB4*av7u``yAA=(M257`bm zJaQhX{GAEux@v19adi5GUS9%$IQhdwK18*Cy9a&m`CR zzKj0o+-HvIc2e{@jWdvflJ+f`dn+8d$R1i{*iMZ#%mDJb@5?<%-rg`(=Jq`Q+*6Lc z;C=U9(GTBrwp%|fx7%{%+~CvHJJA`sqMRTXhjnp1xdpc)ug_47@EG<*b2gI1@oUg6 z@;t?Zz$NZCFx=i2ds zalWCVcxJ`P-{TCB6ik%V^{|{bYeA_aF1Ka76$*2SV#}<@ws{yhv;zG;iVdAb;l5&l`^+X{5MD-mM!%rV^zbP z%MhN_Rkvftd&ljMkE(W~@0h;74*5_2bs0g~UW?!iU(TY_zxRo%zoIF6M#eFF|88Lp z@eBK4MvnW=Az>);y{O0jC(5#-0@ZYp)SvM`~qWP}gYx|42q!k`OXE^j9 z_n22B^FH2~mZhBKCV3?NJnD1C5n7K-eOR{FH5T0$6REYx96f{aAmfb?V<~!=F}0`0 zr0_rE2{WED+9*Fj8)=yz{p1U@J@4PwcKm~XyX~{D{`a-r_UDuh@hW|=He_Cv5;paU zqwC7hYf{&xUvB?4dZmEsv`(mwUrW-)iKTS6CUW zIWn+GAILavKVFEPcvN&0>?ao0R!lBEdo8r$NXBY%qC}4F7o1n$PuL!8i$-Vw+tzIQ zV?eb$!i=gO-RjSNs7s^Ch8a93ci*ErIo8P5Onu=;x#l$=dCi$>hV@u~q5iH&Sic)9 zbp0jHxk?KAqmNYmM<-O2vg0xQ6((}L2D?w!?4@6K+U{6Qp1g>w zx>a;?pKGvP(FeLU_T*=G_>LqK?4z5CEPi({dF9cOwpJzV4|jKRUbRRrn-aA@9uIvp zxA;={2isTfB>+Zxng6(c?m%Fqu{Y(i6TpLWUCAZ?gx?WZ_D5myo-^+U{m1Pm28xJ( zAabAc{#Rp*?k@#5in7z+$$78^d3NKm+$4Jv=wD5GE~nqZnEufO5jpXauwNqH|Ite# zX+5Us_ozxz{So)LCX)P)h&sevo)OTOd%%MT+RNDo@%Clta{m8GnXsj=@*~ebclgv# z^&EbmpfZOuFfJs=`xe$9zZ3S8Cpzsn<_i1O?~CMLUsdc$ot^rX^PFL;A@+Fh z#cxd>-D~-Z&E9eSn>*NRgH9V!E5--mmEiHttM>d!J-3zm$jOJ=n^OF```&+v!!Hvm>368=FOB@oM%AvtNXE z<-XOv?aI2?I35-Lv^bX=8^?p)tDuKD*D%JuB<$c3?VLAK&-w2$sdrUrXTe9pKK4Ye zeMcF;Apw01G=z0OHo-PWwf2GA>ytmM1!s%RUqxL;c4ipX?X* z{$(QhF+S18L&Vd3O47$Y_F~BOorH78U-!Oim7xEzi`ACh%+sCommaIM72pAUPam*0 zb=$+9kT!&RHn2knm_c94zSuio%CkafZxHma<&q`X@V6q)YcO-nN`|7^{ zm*cE|Jgk3+uOsKmkaOWNL59HnO2K407PCXC;hI`0as1}i%Ip1wB z?=|FehT-|pRvvqd$@OBt7wb`(=PB2#By@kp5WvfqZ@>Kub}ob;C@2g zSB@PQP^{MT@IBM^vQ3PGJx`_QFkf`$K;}KD9URA?cjUlejWf#Fm%V$AqF2i`5%XM} zP$J}V+Bf5aZDN1(>f(>+pR@IMu@2Du5=JU}wyg37XNRxni?~8~%;+z+HgY zSRZ(lJk<@4A)1e|0=M&0<6)iNU#?RbPwiKgNcDX#474q@*95O;iHT!{ZLELgOm@sKSpb{HVf@ z9{AB?alSwN=z$*<6*__+73j_@`SHurA2tel_hIM|S&#oCy$-iZ63}HxL|BiE5A>5h z3g-E9k3MxOzXO%vOwC4N{?_N0d1uzQcM~`I6=$Bp6UZ~y*O-M(z}j4{*YVxBc0hDE zFvPzuT4DYv&QF%Ec+q3iqO^6LyqjWn^}MTZjay0^X1qILL%Pb=UY{kZDtgKl{C(g*7_d_IEltpd+K zmG1{-8%FT~#*OTLZ96!_?*QI*W;32upLIr#KgxK1jUIy6ThSMC534-OqxX2&<1Fu; z3eRnUuKBLhyjCmtcHVXI?a(@Tj8ad@^?yWb?$%Y`PwA>3m1s`iQdjlqJEg0>FmLpt z8ObA#9vF5U8akz~s*4v^Xy9AY{tk~7Cf1edt`oZ8i*7ct8|8SU-D5U-J*x3!Pr6p% z-(G$s`t&$-%NU^hQ$J7famv^6E!}sO=B_*TT*3NB?ED^hTb40Q_hWx8;s51NaK^f; zGu%40*TdJWJzijJA#kmrhYp~RK0q%i=%0S7?&DfP9o9?+dLO;Rda7Pod$_ZcyqSug zu#vxW=lG?3@qcoC{#5Z>N0+_S@RU_EOLF zx|%%4wmzDrF9S|F(w^MY85v)w>X)JOb1w5FbKZ#Q_mZ=DU|C7H8VQJ-TU+;hW-0p?(H4;3x9ywhpi`blTYt;^vvKtwY3N0Yw2@HF-z>rAO1Q)|(UX&P|oLfTWnRKiM*z2ohj zN3c2nJ|0@ZugLzOpL<;Vh%)y3qBm>NCCFzdHr0H)ogw2IKZjmFL5K6(O53qZu?Q$ceyEW@o9p2HsjBzvVUBPeB^gZL^|ftN>%Q&NaTOP4pnX}8<{D8+ioG8>2HRNyY!GY|{Q8yIk;FAIbKv$I;wt@;SvM+n` zvAvBPID(z!1xBmk_YEFsPeVRw7I?(I(6BGO*cV>x3orJC*Ba5TVqd7(7b-GTg${g< zjp1v9o}ghzR>a?1`<5d!e%uuwbMA{jxpbL|nY!;vnfs9u?#CGv^_=5zd(a^F0zUxf z1joqJAXlSoI_Eu;uTfmjIncM08^4}2XM56;Jcp)Fo|HVpM$!^z4@)|7&L0jw6}G-f zzFwM!^3P{&Mh;!Uwd4qILjNuXj(ei=EHi2UTW6EIa!!f!yL{@owd8zEA-|$ds1p}M zD~-ZJ{)Tg|IcEY#$Wu=qdWp7n)7DU5-<$fKHS*_YzTxJG z)YXyKRYh)9JA8V8-&TCVYlJ@VA=iEKw*z&JGxB}$bd2@SHaup2d}pDaE!UX4!ZKG; zFCc%Kn4{bSBJ;UznL~|FIpzC#x9jXBjdP;XxzV)k=BuaUuF2KWCr%F7J_V)Ta&ie%owt{}1$&7yaW!{~$xp zz_uBKZ4<+`xft7KI{uvNu_@~D=iDxTKM#AF^)oukC|>W-xV0(a_->4HWS)!z6O)ks zB=lA~=I~o}WQ=t|y`t}3!P)-g)a_d#l0JUt0Kc#iAICWrdpM_pxLbWIZQKqWW?rF| zu-@=#SLm!wllUL33T%a}(ZD*6EQ7X)Q_JKwr-tBn*(c^0Q%CH#-T&`ZavdWr*E+t<< zXCGb1qHQN6f7_g$W_q-*qhA!6i=nd5)XBxrf=(`mTtj15C=u<>sI84w%wrp)I%izj zoH3{msF5~ZG?~3kiFnZ=0ou@>3P-e`g0rZSlE-m7fvjeof`5#&2y6Il(gyrHyRf-` z8I@<2m^ZfmQ$+i<(DyYd_Pzk;E;M1^H}G4r$o5z8jlH-^(O=3|?3Y#vopV48F-2(+ z2Z=-OB@VfpwSPb{Jk6rTldl-6rnIO>Id7RZ8Z^c5ZdY2oi-n=pi56{&FnqO23wFGb zv70kxb~CQq<+$*X)}-b*pFPnVCq%ND!(PU-m+|bQt*K3{De(PX)+7w^ZTf$f{(nvX z5AoY@4Z`qzLuv8wyAtYOM2q?d@O=htD&$dmRZ~nFBWcA>tQL{Xk&ORL7aqmCID_($ zsQKxZvF`jEn7^0#H_&D+{V=B<^OS8`)RE^LMYQsmIk$OqL>q~Xn~qnDYm(ZX^!+e6 z^SW(+m}+|}G&&U;y=(<=k!_dsf}w;1IqgbEj<(Ml>GgCpW$)ARjjP4G(7X4s?tl&( zwJHHHW z^)Go29R{*R$C2G9q@4^6dXm0pS%00~Pvv*(92)cm`Z@VP_!#ix5I=%;sbe=|^Cxyk zE#Mo;{xv-FEVkfXQS;aq&dWky_25T+1vKit8-A>UA9ulz+u+9y4t}g+ayv`NvrPcY zYnlTQjeO27SVpIG!!kQFk^oO9wge(szv9f973{;$1fFSFjsTXwhaR4W9=15Vu1dRp zcy=bgC(Aic%~5l131AfH*Rg~*HgsH+{vm)B+6 zEOOd$`#NF=@!^2`p0c3h{f+-8)h@K%)&G3?PFglHk6+7oQg+6(k|((LoLf(%d=>oG zMY@hNru5w7qPtyjZI3qhr20+4B|2NS>pXYsKP{i144bx@a!%=YUDynJrgvNM8QtyN z`;5e+#c#r?;d<6+_2d^J%kL-WcEcWNdw5m(yJ^L&wT83S8qQj)oV8XtYc0~Ktr6`W z>`M>!rAHm9@SDKx(#NYw-Rt5(boRBoq^;GRKNL;n4@Fr+*1!|>@JAP~V;f>4ewq9s zca8V+zI_^H-L@XS0LR`NU-CHmDF1Ppt8XsZ>%>)cEean-1NQ21tSe?hvuk9G%q!!r zdO-R_TzsYTzGUtSd5RwU&eZ!W#?fXb^orkJuFHj!W5anWZaHlqn1eII|9bc%)@36Ck523b zY&H!U2>j2MJSF`+>ji#d=G8b?|KwwQK!+TElH=QXAP(q?h!WCP>fRLi~(U=UEm z&b#QRsp&W4Ph-qk=l|5s;!MmT{2zSgr*^O%p8@n+8RLo#L`=}AAcj>m9UGr(_;%v= z*JI-V7kF99l=Rv_o-HVC5*c&sxF_>d#{Grqp7}ZS?#AIPIp0ofZ^gW7pFiolkGS?Y zk<8djzS>cbj$Q7YkGC8g{ML`}?Gtj2F6SP$?DB!c05VQUza0DRqWbstblS(N@h82* z*`$7-d161dIQ7c>@Mt0a!*i&2uIPAoKXncY{f2SiVh?ehL(oi|Tulvp?&i1s4zkw; zdh&hTX~Yk~U((-}PhXtFmc|V+b=wnVO{s{8)jwu?u+bLk>{aitnD?k&eWAg5da6&Y z1fO?I5cbk@MC8`|n0fp~U(zFD_KBs6YFtDd{2~$gPLAR{ANGgZo$>5n<@8u6iuHE}vV5#*6g4#2y*61hZGB7PA|zJu>|$#NyRHbY6iGmE)> zh_3F2f5&@=&~5j==g`31eZDjeL?jJ>3%NFvG+>BdxN9hIFV94jIJa-u(liSnFRn)D6KLif(J^QLi7tG9dI%We4*|atw;h9hCnUe6@!#>?gD22| zDLGt^{k}bF9^Mm7`ghAUq7qpKjm+5Rw?6@|9~1ZX9FCet3e?E*-Hh{Cr*4tYFcDhK z`KYrccOm%~+ym|%hwzbSTn24_9JN1syi;E|p4^N1(a0?~M9sl!p1J4^PcpF+THD>J zH?qEa5FYdF=`<<}$b**mJfdUfcTOai`|x9DC_4Ec$;1;;^W*$%$rtL`(hq+OfB6%f z7m55~9olhdugsAN{HOERYQO*g27jr=jz08Ad?a&bMGYUcszR&ybIgK;eo1$FF1X5V zmvmaFr}>cen9GOtN*5olQ+*B}_E-ICWxdji4?lg;|L^C+sLy(ie3j>h!t?xgsqX{D zT&;4pyW|OxcM^|!)MF>}Xz%fKy!YbAy?E50;?ay#@hJ9EUmo?RdGx@)!J`M@QS6{E z%cJiedK(+?cFc@tc{p9uacyS#n;dqUT=7?@1m} z%p2bHCUfRTlesga`p35@$y}sn}4|g$M0p$ftormB`c^*sE2*KCRuzfP0@b zh0h~(ztjijJ%?PGfi94170y6))-#e;BZ)&%c}?Y(JnCL2CPkl*(xdDZA9N0*e1y0$ zVJzVxXQeh(c3)G)x%`8Po%(pi`TVD`HXzkVDAc%tYaV4nzD{{P*NKz|q{`25eZmrA zD=_nja`74E%XmI0^_)}`A!Z6u#l<81VmjrE&;>N+w6~J$>o*ELGvLRv9|{rT>Jed41rNMm=l1U**W+r+$7V1_r+rPVr+hm76H;{Ae}?j5zL-N! z`4j#h-Q#%?{|{j75MMzm7MbX0m^fEo`&BGl%FT|D(CGHMPeMV_yNydka)yZ{IisDL8`qb%6If= ze;egRO?;jHM~GvTU*UOr{08!B^|b;>%l36~DgUgaJlZJ_5VumE2D~Nypk9H=BNjm) zWt8Q7->3ZU93dt+&j*P?Je$mT<4*YuaRKETzOp@sKKOqf-|AF(J^xF5xF1bypj<=$ zleys${l)8){dGdT?Ua3@1lp5j_u~^~lqG)MPlgyFX>(?}>=%`k8^8g>bo#%Xauv^A zcw~xosj^!mQ`}4WL7s2phDQX%r>V00JRm-!JZ+W`n5;a{5|t3XEW4j9F^2L=`b)$2 zTa^6^fQLh`L&Z|cOL$*CCk;_NmnysUgW^TXxAEMipKLK2-(|lD^!JF5aU&A9~^y-Yyn{u<<0l zxt{@I2<2xePjjBoO&@UmMj6x%45b(Nud7)ieGeu%A6exk>CDk;nN-A{oSL-|;b z=hG<5_T5jRxKfsTJm)A}XFl%pfnpV9N$>7wkhqO<=sIu?PkF>(@taiHeLh(Hmhx8k zTf)a9P7^FuowEDyFuTVbBbC+ID7Y(Vh`~K--nS3Amaz8^w z6XoR)cZDPWhKa{gW%v0ov4`?D@;cUW!y`f>yPsQjpNB*qWy!zp=M14!eiUMv$_mS;~~G)bqjpUHR`mFA=9xmh#^Hghhz*x$!jrmWm&w%I@=0@gvH|dHxDFJV>#} zQ)TygnRtq_)FZ+E*?m4zTt~T(`5_)0_^qWZ^@aPX6n9aUGH_t(`7X**-?$%-c$~7-C+YH2 zl%@Vim!G99^+me;JY}gb(&d*ZOMQ_p@1y(xWysbUj}-i}oPMrS-a(n=`~TD4wZ%$O zl;Oz3UV46>OdU;L@TKKw7hS0Wr{veNH75DEM6^MT3UdVC@9?ZAHge_w-r z_?5s%0(rv*`|vR^**lM?!1#;@BX6Dlmm2KD9|Fel8D=7N_|F^c!+!}(_SfSlfN?B4 z$4>&2{jIN!-fw`(-g*2x;PV3fd*BOzeg8Nc871ub=Kz!a@%S~sWKTT40JxezR)5&& zT|}5B0}Z|enEIc`ZvZBHDhrVvm?+!+lAci7?asDI=2N3PnoSMS{FK3d3w z*BtC;{eFMCvpv&1ZQlB7I2`M~oleKo{$VvTLpxoObMSgu(Xy#J+CO47k8e~*Z?XDA zqmi9%UU&26m75>Dd6ThP&;Ft5_$ZW>Qp8FqW^!(1EDNvfLce9XjMo~}>OcfnF_&ec zq|kA$imZc(Osk2~gW2)^_=uglVWo<38Mm4#CzUaItUAak>MT<3Zr;4A&S6eD>yJkk zs%ci_rOT3>JDuiPzFuT|sb#2qJ3V4KGvj{q-bXz&m4TFUS8|nW7uz&3BC|dvG!UW3 zQ=7Q0nY?v6nAzee{q})L!GtHN)h@GX&UGPHqD^K~!Oe6$=?_Nz$xvxanXM{1ineFd z!RSyF-BdlR>ZRt#gh_c`$c!72%!?74$h^OM>>VC&s_jjBkit;}4Wp=1nIt!ABT1;8 z#X5DR=CrwBBu^Nh7i(xss`zqyIM7xOM#dgvX$8={0s2l2hNvK3Fl9@v zg-oTA@w|QQQ&NuJTB({%cSf_p7MX-BEedqDBxjt*GD&>hp9aw;Q!2OI6>%K%G%io0 z=r|QJGg+z7Be*Sh7rm$&mRDKtkH$NrqbOZ(amq5ui$aQ|NXks^p8tg;SqVbfiL@+@ z%2UmZV?3Q}5gNJTn(=f?&1QC*Oq4++RAVAx6=SIcD|qTlg(C5)7jNEp4a|4D{aNn1 z$~kQuPm4qeV+#{Xt8GF#9uIdDHrR#uZ^YXytcHHsda;6q>C+8YK$pWo}p*7j+9m3P3w6OOVFyOYEt!BOZBYExU@Eo zg@^%lYi^R!&Q>zmn#j6r@m5z=V8^d+%9NUeYb^TC_Tb|-s5K##EAlKY(SLI-=IwB$ z52xeFY%m_JDw`!D!QjPRQW%ztMKjvnP%;>i?ODIHhcFV?T`I8W)ricP9uJ3BQ<~ed zoK3A2qp`uXfgh-5ZNcapFr%oVMiFtN=z9F#Xk91nmYQPv8ATu2nPL8OE$&-zH`9aN zlkm|#gw`}2sUel^e&Stw3&oJ=O#>QFOA})fm^+1uuM-Ii8ge}+{a-DZL~B`?RHPZT zGcA3=gblUJtbP6bGtFFy8o;lxC3E~#Q(9ghI84e_$*q-G%E&yl-bE44Cfgd@mYPJ- z`)@`!_=uWNd-e^qT8gY1>G5PcifFPMMOWjO+L$1SCdN#PoF%5PE=h%PK7Tah#JXx` zo8O!0J+&i^PDEynv6wRvJA)y=Y@bM+Tmfv|nxb%diospd!a`$`;x*ZCoi3NODDs4K zwYoGCrrpaWb1v5oMc0jw+UWs%EU=PNqA!3JgHe_u1GM7 z^1S2%>tJ8#q}kS_$w5P3ASB$2wvTVkREg(dtcmKk`q`^D1foLnIJQ<{GAuQg8<*)r zI}z(q@vq-9*gV$5zvwzO8jqsFBr4CX5L}mu;b}^fH?7gMHP*M>L@e!2%{o(gOv>)F zdv(;9R|T=u^}22g)M+^8T+W=733_BMFy}^RhkZ~vXGtd0h}HW4e`r2wXhxO(mn}Cn zs{iRqS)CTli^T8Vb=YM)^_|0xeUxFtqAOK)r($=amYOU3{t-3Pj= zZkNWQQi#y;sMe_pPGRTFN8Qg_xXuaou(aW^*1BYwHO?jYOP*m{j?27A zbyStxQbzXJEShVwk1d3bn#^0pTAfk)m{{#)JNMcUMHWtOC++72%X9nHQ?BLH$2OX8Xvyg~J!y*Uj14VSb3~oIj0D!5 z2>2vU^P$P2>ei3sUWGH5OFH*}N(-$@Z11T;=UCV|C(%zxrX8KXnoL;f3@>HjoMnnT)6|%s zWUgZ;EU4V|veTwD_`H!xT5PB9dOFe6(&;#wfT%@bs2Cd+!pY1g!o|!o#-%MqUL>hv ztZXd&X*zYNQ*O`s(n&zuK|)goe$uxnwWYZ0PZ3Tkg0*HsbY2=Hp1HPIS=eg0)Yy#` zDYF^ZLKjZwHrJ)a9$B1bWg5jnjbU|CG8G%Abz1Tg`#CrpVXz%}f-}S{jkpNdMp~8# zV{jgUuPLO9VQXT=v93w6d!uA6x-Qw1$MN<##QFhvbOOB>bM-G8QWuq% zg|2RC)(W1k{Y*$}Y{%Mqt%in%tjkmjky=)knaU)#m4(W&1*u|DIH7q2vRVG;jnulW zxGHW}GIpqtQfD?=@`_&bzcf&j2|L#~Rx ztK_QWyOJ>wch|XUo?RtZ?W-%fGVQfJ+lM|?lj)j7)%iP;>%QNWyq}!bOWb`m6^5p= zpQgq3(^^>7TKe_pk2p-B;}bey!4Xk&&f-s4FwX38-h&9OXGMF*+U=BR_gH(L56!<3 zUH<}C3w#@o?^_#c_4z9Op}G7keP&JX^esF2OgxGE(fPOe_~y6a*S-L~9(|ThzK@^5 z_o=~WtaILb5l-@mN8hkM)O^PZSY2M;hk-ngeB17^wSP=Qo6Gw`z$2fw2XQ~`NqBmB z&jvj5bvxl|e>_k1yvvZNPnUe)9>6U;DUi1l@W>bLaon>O-s1s}eB$17sP?PzRG%(K zMfh~d7w$RSG#KgM?P`qeyly+YarOCOTKas;%-a#I{}Y;=6;L&2Cp2RUcVOt9{I|V zzuMU?yz~`|bSocC@aQ{le|dQ)0v`Fs-3}i4&-u&qo(g!B=3~3?(pN6hC11JMZMDn$ ze83|gz!`XL`FJYekuTj{;N@O9JiUIGylRmy`Pe;*oBZtj2-LEI<2Q3~?m0v`G1y>X}Z&s)yN*8(2-=p8r) znVuBL`)9x-U%jVspJ?F;*fOsl`Q(}7D8H8e@#TO=zIo5#zNkgsp93EG=$-Y6+KP5Qbt33ga{I&h%d6%EF;E@mCV~yYTGVj2-3*Lbf@Snl`s3+m+<$W*Uy$H_z zpRE1-Jk|5?pXZvF3;FsT{#5NhzRdeVz#|{P+wO+17uv4Byu6OT!F%pO8}uYRJ@2Ohk9-EtzZbr$ExdQUc9Aam3O$D#O=y03 zc_QGE&& Self { + Self { address } + } + + /// Call the main entry point directly (no routing) + pub fn call_main( + &self, + caller: &Address, + data: &[u8], + ) -> Option { + // Direct call without router encoding + call(caller, &self.address, data) + } + +} diff --git a/crates/os/src/bootloader.rs b/crates/bootloader/src/bootloader.rs similarity index 100% rename from crates/os/src/bootloader.rs rename to crates/bootloader/src/bootloader.rs diff --git a/crates/os/src/lib.rs b/crates/bootloader/src/lib.rs similarity index 100% rename from crates/os/src/lib.rs rename to crates/bootloader/src/lib.rs diff --git a/crates/os/src/memory/memory_page.rs b/crates/bootloader/src/memory/memory_page.rs similarity index 100% rename from crates/os/src/memory/memory_page.rs rename to crates/bootloader/src/memory/memory_page.rs diff --git a/crates/os/src/memory/mod.rs b/crates/bootloader/src/memory/mod.rs similarity index 100% rename from crates/os/src/memory/mod.rs rename to crates/bootloader/src/memory/mod.rs diff --git a/crates/os/src/memory/stacked_memory.rs b/crates/bootloader/src/memory/stacked_memory.rs similarity index 100% rename from crates/os/src/memory/stacked_memory.rs rename to crates/bootloader/src/memory/stacked_memory.rs diff --git a/crates/os/src/syscalls.rs b/crates/bootloader/src/syscalls.rs similarity index 100% rename from crates/os/src/syscalls.rs rename to crates/bootloader/src/syscalls.rs diff --git a/crates/os/tests/allocator_test.rs b/crates/bootloader/tests/allocator_test.rs similarity index 100% rename from crates/os/tests/allocator_test.rs rename to crates/bootloader/tests/allocator_test.rs diff --git a/crates/os/tests/memory_page_offset.rs b/crates/bootloader/tests/memory_page_offset.rs similarity index 100% rename from crates/os/tests/memory_page_offset.rs rename to crates/bootloader/tests/memory_page_offset.rs diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index a6811bb..b992162 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -12,7 +12,7 @@ k256 = { version = "0.13", default-features = false, features = ["arithmetic", " types = { path = "../types" } # adjust path as needed compiler = { path = "../compiler" } # adjust path as needed state = { path = "../state" } -bootloader = { path = "../os" } +bootloader = { path = "../bootloader" } once_cell = "1.19.0" serde_json = "1.0" diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index ccf1dac..d5a06a9 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -179,7 +179,7 @@ impl Default for TestRunner { impl TestRunner { fn load_kernel_from_env() -> Option> { let path = - env::var("KERNEL_ELF").unwrap_or_else(|_| "crates/os/bin/kernel.elf".to_string()); + env::var("KERNEL_ELF").unwrap_or_else(|_| "crates/bootloader/bin/kernel.elf".to_string()); fs::read(&path).ok() } } From 892f58fc79132657c9564b05ced2508183b87f91 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 10:04:34 +0200 Subject: [PATCH 14/70] Add kernel crate and wire create account syscall --- Cargo.lock | 9 ++ Cargo.toml | 1 + Makefile | 2 +- crates/bootloader/bin/kernel.elf | Bin 54872 -> 24764 bytes crates/bootloader/src/syscalls.rs | 47 ++++++++- crates/examples/Cargo.toml | 1 + crates/examples/tests/common/utils.rs | 2 +- crates/kernel/Cargo.toml | 20 ++++ crates/{program => kernel}/src/config.rs | 2 + crates/kernel/src/lib.rs | 4 + .../src/kernel.rs => kernel/src/main.rs} | 89 ++++++++---------- crates/program/Cargo.toml | 6 -- crates/program/src/lib.rs | 3 - crates/vm/src/sys_call.rs | 1 + 14 files changed, 122 insertions(+), 65 deletions(-) create mode 100644 crates/kernel/Cargo.toml rename crates/{program => kernel}/src/config.rs (97%) create mode 100644 crates/kernel/src/lib.rs rename crates/{program/src/kernel.rs => kernel/src/main.rs} (54%) diff --git a/Cargo.lock b/Cargo.lock index 28662d9..3e9bab4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "bootloader", "compiler", "k256", + "kernel", "once_cell", "program", "serde_json", @@ -244,6 +245,14 @@ dependencies = [ "sha2", ] +[[package]] +name = "kernel" +version = "0.1.0" +dependencies = [ + "program", + "types", +] + [[package]] name = "libc" version = "0.2.175" diff --git a/Cargo.toml b/Cargo.toml index 48e828e..19d12ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/compiler", "crates/examples", "crates/bootloader", + "crates/kernel", "crates/program", "crates/state", "crates/storage", diff --git a/Makefile b/Makefile index bd9502d..4e82ab7 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ run_examples: RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples @echo "=== Building kernel ELF ===" @mkdir -p crates/bootloader/bin - @$(AVM32) all --bin kernel --manifest-path crates/program/Cargo.toml --features guest_kernel --out-dir crates/bootloader/bin + @$(AVM32) all --bin kernel --manifest-path crates/kernel/Cargo.toml --features guest_kernel --out-dir crates/bootloader/bin --src crates/kernel/src/main.rs @echo "=== Running example crate tests ===" cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../bootloader/bin/kernel.elf" cargo test test_examples -- --nocapture @echo "=== Example programs build and tests complete ===" diff --git a/crates/bootloader/bin/kernel.elf b/crates/bootloader/bin/kernel.elf index 8219a792344803cc2004ee8a2bb439cdc2942c7b..96a1cff296e26f7a751f3140cb4149df4ac8e5b0 100755 GIT binary patch delta 9289 zcmb7J3wRYpmOj;e`{s2M5;{DC5a=W%5HkrkPhs4f;Vn;fKt)i(O#(s$!VZm zuk+Wbs#B*=)oG4x*LSyxwLNbNB@&eq(aziO2c?vnk z7+7!tD4h}N0jTS18Wz_rS+un7x@_ANXrgNqh%|XT@tCYjwk9tIe*s!eA-=YXGSaQW z#&m1YW9e3Y{|qayB*V(7&aejDm|AjnGvT$*)67jt&mudiHWNY8_@}h|EllJq}W~`>HBghwy%`TZcP-ddxgyIejy)U zA%dUnqn6zLBHpl#%%5oP* z*g|J^Nf0&#DBL~C?cBQB%4-h5Myb{y5eTmj%B<{W!&%6Nn@xW)7INjv zPo|}|o4!V$OdabBrmU@&De2S#w~@;u@BFgDpyL$@W;vzWm2 zEBh>rxSd`ZZFfpHrs=tDsd}DBiw$f{?J&Cm|98y>1pzCse`V}49J`AEWXR%Wh9 zzyzYU@&)bB9Jh-4>!uC|N(zyg9D>Q)v4z>}*_X{#y(m1V_O@{Jw-bX+r6P6%CUbns zZF1FZBHXa#mhji}Cdj6L)57&Tr-r-!W$7fj;y<3pNu~_KJ|cASfg2ks`YSMDi#L!Fj6~Q#7s8mWTJ5}-gFf1kc=HnRbk;H zX{@I3lARjfxbf`Qf6obhXme)mydivV;b@r@5Rq_0=~zT19dDp+<=?1>|MJ?X;Hp{? zU$sO8S1%WsC+QrywIs}}Rl@AMl7goPiQwmv--&oQ zi-PlCB7N^FiXD8C^g}O_et4G?^fs=v2a)SEVVW^kM8VJYQ~dNf5ubH41wVZOgB&9L z$j2BIt3CJ~nOP~qyk?Cs`#nnXnJ)I=!FhL!V0Zxq&wVRmM_`GADMCLySi~ZShB~V_ zcc+MDw*|~=QiXhSm56`wqKFNy*5Y4|6Z#;b#XlQ`RVIqqnDMYuh8`O;ovgwP;09QW z@dJbdM+9eoB;sc*5v<=QV#5R2f$#1BkB(Tb(BuX~=y`3t=fdpuuh{oAp%1L)z2_~7kl80-n+vNZ`RinR ze_{ix;RhEfHn2^PuN*Dnb+1yeewpnf!PAFDaLyX6G7DR9y9j=YHwfoayzY$c3)V~f zzqK|!cgcG8qf6G7mP`K;{`{(+UgWY=ldmgm0bxMV%c zH6rgOuB+3afN`3so4hLBinb7xF;|1K?_;@*nzg;>-Qx`*_K?y zq9)^3)87_o!C56&jGAO76qA)Nd@=zOE^Nlu;P}XH!`AG;@sW)!#dZ!jfh|4nv+|lT zF+_Lg#J@LU1Lp|8-U)6g2r52u2(;8RQZS&mt#z?^C0=AUAtE>VJ^iBhn819X{-7(g z(7X~aK)HlRxiE% z$XR)8R)6t`{B~BKJebI!XgK+mGkSqYHP-}aLHneOd@-?)Z`%a82Ci-FFZa}qNwBRD zS|$Va>ojAU+*F^Zg(k^QqQk}KGP`kH^(lA&oeUV?>bcmUTp?T|o@b5D@78uconl)@ z;v#{Y2anX$G_8k8G@JBoGB9U~GfL|;IeG5&@ojUai)0+1{>dyycxvn^ht0`#XEH24 zdF}?$#oh4=zM1&<)yoU>uG8pqS=>-w$w1DWijdST+ErJ=N17~Ke zFbB@i&{xf{H{A_9A9A(Ajlj`dqSY8!qinhad_UyKH0=I)`28C^mWwCj_k(yi<755* zjNkXck=Sw?4WKt7Xt;n!t>AO~egMGPlCIPnt0fE=wLA2q3Bb!CFSfBs4*`#Xx!B|y zrm-8CE$S45-UKcLf3hvlpuYjvLVn7|eu{)(K5lS|pN0doLZ>9qC}7?*rzFx?;7w4% z6XX>w23`yKPGF6a=sg!Z{3QASm@`>;Bjm}X!BmW$k{lre4bMZNvcS)P|9~xsVy`rG zn+nXiF3UBz-ewm&3Q}krFsIDU9tX&WnK5=sfD(W?ogQXySt`wTu_KS9(p)r*fRi{g zOrt$6b_i+I3fzhb;-9_ZFM!#poYIBP0(0{143JKflN{{G)9Gel-U6p&&{W_CSS1|| zB7!wV(Qp9o2=oB;pk2W115W8mZvY1dqLVG}PiKMI2b^M3JMd26>9#zRh9!dweGcwU z4*{ol;BIsjnE71b|C#g=8cwMhVG0^vaPHugZiEApGGS(W#*h~S^8iliPC?*Jp8j_L z^MahxgKB`^2VM>5*07OlT?{$KM}tJebnH0~pdnm*>|#ejFZweuzm`tvO(%eJFd?-i zDady>LC%ZP@Ul&DvBS@zTY=f9oYIGG2d?aoZP78lM%U1Kx4|J?LmPo>bBLZogGPPn z&n|Z4edz=+zm`tvN2h>~VK!6Iz;8tw0tT*>9TuwuF7{Y349s!FUOyp_)T1E;!U0#o zT43IDr}QTYoD2uS!tDX}0COO5$^dEw{t7tV_Hv7o(j4r_EeZf%%7@>e0b!1AaWUH; zC5NV=ArS-Mt+NMM4$Ps_DY>)~I2&>{F|Tk3a0vV96peNQb0k%`1(+kJ!mk5!q*Qnh zFe0NnJ`}d1fkUSvybat6tU~iy7b|%?u=<$B6Y32jVNZ7c2PFLxpDnua2OBKLGLy zR&#@g@F?&m4_*h%ky0tx0Nm=qPXM#iDfyGY?34;W4a}#d!p*=D^{tR6a0m^I72!SL z8Nl!@dxGD%SjoQw=18XS_rU6FBkL=``^;F$3xU^p^xcw<{pSWnm<9p|5`|{~s}Gi} z@F5o~`6^)b>5}EIx>(6y1LnY{@NQuKDYgcY^vcc5IC?Xycf8@gAV}P-$Oy* z0sq^@ivK^r{GKcPzrg%jD|`u<-!g?SN6^6UwQA7d3_U&A0Or?D$rIUGI?DOWDIV;j z^OwtjXAV+?JgW4_3G$0Ls#(q|H7$=KLp=mmQ0KwiVU7p0e4Pige1iwG{7nyL`CA^$ z@~=D?^2p_Lo(3lL>bQ*XD$ZZ-%lBv3_Pd90t4w@;Dv?CBKfq;!-xN$ zzkCTM;SPy>;m`)s5L_O(!&=)!`SgIN0}rsxgLwkK^i&`*y456F~zq z?vBEQYdx416niksr+F~TXLvBnS2Dg|8L5l}-hu1xr z<)=KD<)?u|(3Y7j@7O%BXX%`ph9xBxb1G|Vf)%y1YRb!I4XGJYQWJk>^E4q__g2bZ zz0o!P_|^)ZDRSnx<&Kr8vfC^BPr)T{#P`}@{0Pp($ezElFHJW`jYb6in5CGs?y?cRZ&T4XVm$fbIUs8c22FldTJHPOZw85{7ymPkgHc-EZg1;$xUze^%a#=lKk24XXIb)3glO>-8kSU zcV~#a^YyzUKS>K-^%}2!ZDs6iW%z%;1#-lWpBG&1x{BqFUDwH%cMp~)cU>>{wUo%> zJqP$bi2v%1$F#(fD#t(NJqHTnRc$AQobpb0x$%Ww@riFIYXgGCrQw=jXToZIsIY4;c%nl~eYouRGw6>j$0Ea(4A8I8fCc%1&8!Gn{vqbPkG(-n`i-fz0%26C!{ z_JhJR9wMj1pWx|8Dj`z}2Sb}u$Z2vrp3+YAE%xv@UCx2Boi@+CfhvW6@(?*sK8A-g zQ&ki%6Q`9LnA776c-)-MrYhcS50BI008(MLf+`(1;6olF=gUvIIdccm{ZWEX9%^7t zn~&k?Y=RGac$_X@z;ngGuD-~550Ufe#NrOx4+;n44y^{}ym<_GoLj4+6ym#R2agBc@(*|?JiLZp zy#KvBS2zrAuLkCPdOCQWtz?CV$9Z-wc${miqJHl3@HpLW$HO<^j-rT3xU#B&?Yz5R z=U#oQhsXJMDR?kYM^XBo_wYCm9}iv>{>H@~UlBj_5II4A7f&JlLlwo#gc;NXIZyuv z59jKtC|-?+$NBntJW)kL4W2zdZT1j3G0)6{33&v!D1~YG)}jXH{CpFh21P=xc*8wB z&eH?=&PP8*RlLWOFuqdA349bpPU2Nj#IHRHIgwwB+prOxA)$&gai$pW zJYSuz!1YL``}Oj_{L1>vsh_@oL6Z>jtEHY_d~neW{ypjAJL%5<-1r{|{0{{F2LfLX z0+%icnG5QsFPc4fR%p6u&RaZpQD{1UGiEHPTevV(8Je^(WX_wrXu))I(N`!=oHwH` zv~c!~bzcdEzWe<}bqg!IYR#dY8Q%}pEt$P=(Zcjyr}4TOfBfmn?y8}(;k;$m)y-HK zns`x7?Uk2Ix}x^V3nqPI(xss?x7FbfARKd<$_}olaeaYn2G_bJb>?Dcv2kMZ z#;B4RZk3Sxa%krC*>mb<%$z-U_QI>FntGV(cHUfb`uyqU>_y)X&AXv)!OS`HzQ>s6 zBdq&zT`~(@z?Gqj+xS0pwGh*|)-G5uZvpqxA1U*v%@$&MXy&{H*G*rPp3dw=q1o5X zpHp{T-P}cBWZvA+g1SYEnc9rd^tqur*-GepSJ%xAExNic#60U3%=x~Y$ihWH{<_eN z*)u|O=Pl|zjwSTtzs4QMb#>RxTk!qR^f`0pnQr^@W~Q*1nzj%XD3*N27vq}Eb>W=Z zW?g7Doh_lC=?fMul;i8JfrYbml%`)*-iD zZh@&;i>?l>rfz@QXysbO)qRsLQZ0-gGd5CHJ#PGjGpB#ooKZLP72eD1@&3%|a~9Su zTCfBiA1U*)sb)tS|!tH)H2t&UVzRaaM!s~%rHq58~mqsNUI zH+EcPT-CVhapT5~A2(s#nd3)~A2WXJ_{jLG@zvwUjUPXL!uT^MjGi!N!q^Fs2~`uS zCybjge!_$aXP(Ir&!qb^Y5Gj6ohffEnYFlX;iAy|>2qhB(-$tRTj21M%iLcH&7QF& zbXI6$Y;tVkq)Wef^7FbGv+63ZuA4pU>P4ZieJ!-GZqCefab#%B$?q>*U{*rN$Ti-c zv2c-FIQhL(wL2FP)uVBcA?1VoNZNHj-T#Gz_>KA8mxr7-Lk98D&(&Mj7Q7k21=pk21p7kK*|#o{us@>qi-t50A80_=Fzt z3A;fPdZs3JBG2r^l*zj9=fZ~1boj{jN1eWOEtaGE+A?h3S$mrh@5I~gHbS@OxOKFP zb>H*UQF8Q*XpcHkVIJP(u@NXbU!zPZFe;P+UE}_H8@%>R^QHEUemqP|q$G*4bYX zdVWGg>b48Lz^B-28kNYQJt8urRhW6+pln07Wt6QiR7+ynM)`W$d$`bP@9+knJC^E8 zbzf9DdHhO_tto|T<8tg`1@&{R()!f+7f;cBx1ChSPGpF+z?ZqctJV%;oSBSsIO9C? zl;hl4^0<ZjcRxvO1{M5Bq$@=k)X&Bg9=LtOsjI_2B;}h|?u4#Ve6V$al zp=U*b`T2@@`zDmnq423i&=Dl1=-CdE}c4{m(Fuye8X-nJ;HBzuV4>WV|OLzNbV|dqi00J}%VS z{Q~;(skQG2qgY-)5k?W$iOYDlRv2MY!6=K(K?!{}sPYwI-M-Yj7@ zDjB#!QFlY{yP@|z(EFaxp?7E~%`4^rpO}}|2G8UHgNL)>IgN38Z0#HgSFcM0ZRa}u z%y>YKZ&XBkk9p@qr|`8O_?=TCtaGa2sYVf*xlibY84i9U@4O?Da~=o3kEZY&tuc>0 zLL2c4iBGdJSZpH$^u~3a`m$<~-0_UC8$a%he7r{_eYeH*?>*ib$=Df9uF6uZA|)Gs z7WT42+FCm-vizt}{eEFy==VnapG5T~6GTUU{|WQR;~x1e>F0TwUsw+=7m;`UN@U69 z!YuGpf0^ib>+uujv9;L||KVtI*=NF9o*~TN`2)KDSf^~i;|^cvJP{qJFFBUG(~nG8 zer!>4xnHrKpuby|Yqn=J*UOoENSFe(KV zV|p(4*`DWf%%NQGSgz==ERWg#!+$s5@Mi*(vEB9B#`LI{Yvx?;u|3gTGw*VLk9x<< z*JM5Lo*`l#@2>qDvN~Ho=FhSHB4Bz4YVi;e+RHS6!ZoPlV?@4h>fz>l%Y8UTJ&MZahy|BV$2VMzsy@=9g%oFTbdd_vRP9 zaG4UB`M8M8YDES~86{<8yue(vPhK;ul6uv=6gd^0Jl!a3_nHE}*81>u_<+Y9V{`(t z#Tlc+*A?qM$QY0AOY^n(ja9A`nvH>h=u!<`>P63H8^yN=)#6y8eRBYw9iytRzn5ov zg3K{X*jd@=Rrn{rLfH8ih~%n`Q9Bczo4rR^#XJKJ=3m!&^hU3+N}lahOX{;zGNi~H zqIn{Tba%Ts+_e|-hYNksBoWazMI(t#LJcSUR>^Mv_zV$G z&O9Wn(stl`)Nhs6t47$ldWWO@zmMWSWZdW^gvire#;%lkwVT7jUUN3^DN!PgJEBGrGCdJU z>kv&(-0V*#8lp*UyYmh(_PDgsrmAu-_iYx8G26I*voJQ?r6d~~qIz(MNH*Nfede`c zgP-TB(dt}1#)BLD#@d_voeI9-@&k0BYLs29oEoO_3-t_iC~!0iwY)T5bZxRQZ`$Oy z%3~gD?|XT6Vtc1ixHC`P*p#KVK9yy)27=ZD=cz{M1CRP(lWINi6#uW!VXiqw@e4Wd zIvO!>eI$)Yjk`ctrQ6c`@2(l-KiagWpb^2d{b4@}xoc zMkQV6o1RTU+fQ5CRmh=!irIft2tM^#Q{HBr8{Ki1)CARVL(mHE3R!HOz%}5G_rdeL)&otS zH{P?l#_GWJsn3V2JKp)-a1Hbrb9A2Wo!ooOYn$M!9^+1&hm3>%q4#9NHt4olm`hf8 z^_9Dz`QlhaODOu96wR;L6V-j0@ZiH~dfzAEBdFL%~r!T*;cqA+bZ3a!oOFuN^7*w!GCEl z{Ab?TsrwqVf1L*6-*RbSS5J6e(G&l+4;n3exfeXum*30NyWqJY%G?zRADf_odDE%k znrs5DjRB#r@I&)y*tWIl+KQ85%X=w#@_(R1#rCtdIVF7W_&4CYJkKgsJj|z%#%m%jWoyUM2R8HmVBR0h`-3fHo3$Bxp=|w7#y!-iXdf!qZ)Q`V zMrXaLFT<{DU_BMREoh-%spnB&-JNG6f90|L6r$UfYs?;A8WKU-G|{1p=Lq zoC^FB`{XKY%qNH1IP0K?p|%DMhAu8KilF1-hl_R1StAy^Yc??u9h9pFS@&J`cAl=) z2z?;*oiFKIizNf#Vqk?ZZ~S{u>h?%uDRMnOX6K)c9tdNbw|k^)Pp(APBiE&_H;O;- zpqCWZ>Z%KS@c?k}q-41wWjS=L7#lV_vQ73O+ep_>;j4u@!+^msU^h(GHST`|UU-T+ z-hS|jN9qh@hZ51ID7uzF_oj7+xX&tU_gJO%o-|&hK9)EE2dWNj0T*=NsmIi_-=udn zeWFuZxA~0+cXjFQUhO3cStAWl^u|(0rWp_J%`&DZlafg6|%EZUfXl6G6nvD=8D>( zyB7vCkJIhoot?In|DTKFw!h1IdczQ*zW#2Wo;X+NcVeqGW2-fkxHv8>#;#nMZRgiu zW3G!yn=Z2IXtWE@#S+gOd*c~>V}w6Q>+9v856_sykKe`fM#ZtgpyxjLZs-a3p7;h1 z)YrOjE@{QZcYUgFQEy>qx$-ZL-S7qQ+HglNyr%I94XHNrSl5PN4<3WJD&W^A^Nh6* zg41o-s;}@&;xryh;WSUmWg}FefK&O*!RZ&nXML_wv>ut#p4$_rX_|#EyKF;q{HQ@BmY=m0Pi-V?0~oL`cA?>l68%u8j4TXEumJ{`}CnXid7l&+4tO{SjFu=dvybI zi)HBf8gNoF?B;~R{4=D_#R@BadyP*4{~2=4YL&+P_}B1%wEOje_$|un9lwEvkIVka zQeheOnYOJe>Zn+zKI~S-8r7a@-+QaTehKIUyox$p3Fw1XC{}rWAo-Qa!0qBJ3!hfR zyhMro^>>V`J&^qC%Ze3J3e}2OA@3DN4sI05x;6MG_k)-AtjHnO&F|26X?<4moexFi z>K_Z_L{{?MVM_A5D}*&MOHnU6O|dQ-p{O-qRjit)g*xf?!aD!YLjA@+M8uqn9Xve{ zsoSd<gq^H$9G>$9U!(6OlvxvGLmj%!&Iy#xHd~ zeyMNax4RbK;SYp;Z?2*bXi%7&uGs^Yzr=j;cOCL8$yskIk*km3TmG#`Ui~BCJU?^@_#BgC z9f)tdzzDrC&?s6z&?xRV2w&kqqr85QQE~AgGbek1Iy{!Gds)8+>rb;TeW=P@>h8>$rZiSK2O$ zS-_fB#67Zo)0bJKpEg27yfYNF*jue<{=L(>bVF6gPrS$EdR)){s?blnRwM&kSbJUy z?JI+IUtD$8m9BmNFYrvjwKtS3*WQR_Nqa*rsn0T69s488co3Un^IhsR!B%Z2^Fpn*%Eg-ezT{f%Xc{YHkOzqw4=`jnQ8hr)(GyR^ms!?2NgU0C1S zC|WYV9X0}=gL=Y#C!%J>j$-DJzHu7{}OHq{xoc4uL`&1jx24-{;br< z`9ruRhqiKm9R{ZOrXONnKUIv1XOaD%p@Xu~-N3x)As6Pw@(Rqu4|(iWr^_+-@|nyN zlP_a_h1LjQSs5E>jjA80ese>WI;wr3F4`*l(m)FTwK8U96l-B4U#oJ~&7lKXM#UMK_z}>f z`0mD78I>=T8KDE^=yiN5^&^luo)lj#v_{mRHyciC%}S7;crshNJU_!?(lLQl;E*d?*j&>uJw1!uWeK9pB7k8-J)?fQ{{&9V6bt z+H?nPhDC&Ri`|S*tZ}zv0~EOP@=hm#)T~ z`Rke2NcZ431`0F!*|CJniC6rFVt-HT7NT>3PT*%i{yiOjBa z{GEfO@7M6}^%$Y;9@<8ZIM*P)PmdAa?J@F;tC7F`&ES?HYDImfu00cmzWk2wEPQ>*kD~vq zmhRHk67K-3q-Fp8Yzen_At*At@X2IkW z$>DHm@W*zXwO~(upBohR)Rl^USx~VrTPakz2Duzve|au)l{H93d!~NLO6;njqJL+l zr0IbDMuTF+k##+70PF{2DZ0I<4Y0QXS&Dw|VFNJ7%t+(YQKPY`mmIYVTwB&yeit$n z{}#4D%Zk^Oq_&H7VM2}gejjz`(6+#iqvJ=B127^MfZhW==cD(T5B5-IPdldXeEQll za(=QsV=3eAH5caB*Pg*%>N^j2{N47=)$dwo2>kyDHU*FC$OiMxGw2{NnQ4I|_Sh6!z38?5R;- zTz+55`!%DmyGCJmjrxztZ}_vf{O(XoYseXDD8vpb#13L@QN!Bq^X#B#sg&P(*4_9I z8%BM;3~&1b&j+4bhKtv~kSvh?$Am|C#pv?s~A@pIQ%Qy6Zu5VVw1#5|Hb`Wa93qp5n=*_GpR+pHA^${43b$a!vAu z+QPOW#!??p%i6JRuycyAa}4Yp13L#>B|={oc8nU14Z*YvimgY83OHAJ)RR znfPiJeHYXBu3b-B52f#+>hK0>dlg~Z7h&5MVcQqU7>Cs5rZ@9EJAj{| zA;|yVQpmshf%>gYRo1iy)<5_nXKuONZW@d~jCjW}U{-lM^OZJt6hAumnzrJ%y-JvC z9vv!k>A=}g=N)o4jIuL^8|4=dH!9YbF;46WW%%03GKLx5sulyU6+?vnQ`a}HPTN3# zyTaJA=oxJG!Nf&YpN0Qpjxax!zH(wD!F91@g80K4Vk3@SUxB~q;XuT06##L#67Vre5>d^t(I}7{Obe4_WKOr-Fv!FzqtjxJ{{JFj0E=IfaeoRGAQv&3}pVuTp0tA^pf;$ zO5v86h=beWZrnmkF6^Rz@4{`@y|yMj|7$`&{W{vOf<_(!&og8j;J6AL&%mB&&q@aG z1{YhfnZa>a-RKbq$CSI_+?F^soZkn|TY>YV!1)>A{JNx#6pj;paNPD+;N;*K|6Z1Z zW7(%e`)xl?k2e{32wZIH$+))4>$B`&7Q9TZ6!{{NmD9lYaB#D( z%4ptNC2=O{!HK8bJcn_f&zM6oC!R8(vC4_36t5}m8BdYCDRD04lRBcE^}j1$fPJnz zhxkIZw7oyMxw^WM9G#743$+!Wbn6~)hW@L>4-jgvFp6H_dmzvu*8U4^6jR@oQ?3k{ zhio5Vm9&q*UaPRev5Hg5DdbYefXp4{fNb(`CRgLPnb6aBXCZ&dH^P4H_S+a`8>@|C zd|v~yq4`+0>D@xz=S$SRo2z7e*POAPa$eXTP(n@;@s<2q{MSl}(Oh1gOf*BI*oHsb z9ah8b#l}{7CH}lwUSq{}LA5X*l>Z}R^6Q|py-~)VZ8sLMR(>I9wC)Z^8t_*+vcn5q zrSTrUsxMBBqD87vyaBnf4Y{$)lWe#Y-SaZEwYu8aFs;;BjgG+=r>`Q0dU)79a{VCV ziuxK?W+KZ2#A=}ZV#m)G3Emx(ykvelCqwd;UQi)@1JFJE=ERd-A4p0^?E)XJoRq#~ z2Pa~xj$E{x=TNtbyrUViepD|!3z@kGT|Eb%-Y=Q+Civ)3N>7V6>_BL)Z(Gre({0H% zl1{9=hfH((yQnWrR4GqSA=7McX0>^Ej+#ttz=vF)6-jJ>-V@FoQ}fxWYxo|u5yV+} zhj@f>-!AN+O;zTSZPJd&lH+#e-Bo?)Kir%CrJOtbW~1Faj&=4-=8O;XHVu9k@L62y zK2`n??FQG5c5<XFL4=W}hHc$$q5ocUy6@1nE2)^+!% z>R&?qlvwH+x!q%mcejNM)(1%WiysQUQ)_Fy9sQe+S>;}@twsGt(Zz+4>d8urw!WZe zE_5o-Zh`Yoh`c2ye?lpia>Kyq$J`|DrgNWIeLa4r_4xl3DVw0-!|A!zq~QH$(<}4# z#1uQ|m9&|ocgx>F?z8A6=yBRY*0t_tzK4Z7zoWJ9#KE0g|6*Nxn4CW)$8A?!1Kl_Pcf4&hK4!eyZ;FbGqxsC#l2Tycfn-j7$x7;SB$1 z8gFVztYBw}itS1a`3O00p|%M|<@#(hkR7y!$Fh;#+19tVRGINCpR~>NyYS05v%U}1 zDtZu|ENvu5_jT#lc;8r(IkcJFQe;rzsS|qe0;QuMa9rjTV8e9Bk!ahDXZrNhM`BBD zQPdGx)%FVFNc2tI$&DlVu1evkS}OiPlfg_@uLANX7~IuJg_fJN-7#bn4j6Wvm-A{1NRJ@Lg>`wS+#G zeL~)w*01BPj1#()O?RA&?)gPD;{OG@`CQt?&L|-Uml~TIUheM*_&&Buo}gVfKX~5Z zD?iqKVu0MwWL>^HFMZ#d@@}5YG1oQnHU9XEV``a3yw5M?L$KqHjN{blhrDNx#X#qs zzI}HL@JaZS*W*>l<;3sa0NhUCioWf4f8GITh6zFZz$W-lKW7&J7!3|+-Ji^(YK+KRnQ4n z{|{BkhsYcBO1|V7_NS~bfF3f_bOZkVeQ_^sOf$#lmHaMo!u#+q`02tSw1C|tpP61C z`qd{oZu6ax_&C)&9eKVJ&?7V>24Hi@Yd={x{cg7mPfW7==(tnL7~~5ya~pE95MSRw zbrf;U%KCw7c`By)qlfYw9&ctX+B^xqBi~`=R$(6BG*VAopd_`g#n6R8Cr_ujHWgzE z0@oL^jPe80FPfFI`A6zgZdU9mS;QxACPzcbvZoSDzKmG%Wy^^p#j@;Ih$B_@;|hZMHMOb=U~Uh;3h=g?-RFmL9+-;L{G<*g)wxY8oEq;Yq{wrd(mM*0)za+G#Z3 z{a+_`+IQVke4qlKzO+xsmmqfP!tOq`q(*e;B|f19cBlLNPHd&#Ypl}x3i>BszB`6$XF2|ynw~Uj z@4J;4J^uOqw~C&=`kwL6Q_vzlZRo2fElz=E8=$8YEw&`^@lS$JFG7n(eoZQd<&KjzNUqy$h(fb_O>8WOU$Ey)L_JB%0gZ&-yudd8c z$ipHpQtHh{ax`#Gz0oMpT#(L%@4n?JGy+uS7SyweXYTF zR!n|E*~5NE=R0vubb8qb_<5B~Bh-&vpo@J*#dM!he!Y);mkieP8AkDe45R3S3?~li z$lmzRT=}?F`YSRGbhTR2o+))Uu*fo&I@dspbH(RjZAEwA#JlL#2Kg-JYjLjiJ|p4O z^|d(H`YfZ-shiQ_Tw@u=GFh(AXyGd5DE(weJx)JBkKL=df(^G16eQo=re~Q*dEv+4aXHDf!t5Lt|040Pn*M) z_1GpWhG1u)4-%*0V{vT_<38m0{d_m@ZRPtQ-_3lt^1ToFem`=31MHdExMWY6-ZlsuqtI||jNY-y&z0%Xn7u;o zYmjltQ_1x>YbqI&Je6E;>;FaMgQIV|Vv>DjdtCZqdgXfcl@IFgi6iv1!-EeIJ33S7 zO~hyJhL4ZUCN7Chp83_7*|mAV)%ksqQe*DsXBZ?o~pdUQ?2saPF!*;>&`8- zk>-y_T>Dq@hpXeRm;SUb+LlfZZiMzC+U751yI3;s)lV)}{xy#M;@Cq$;zjtoZ0XOE zu|`)uf6Dm()!YtzXK@QV-;8hBKz$jj#Anu3pEa~XoH9n7vc60$Z7;(%4YRHxUP-L7 z_yzox>~$bU*x}KN&=u_6VtqYrW0m!4!if+1KmN%|`MKz-}TeYkI3xuvI{U%%k9 zPCaW^*gnnm$=EUuRB)ZJrf#T;Xp4z6?T9)tpOxg=HaAsUwOZ;7gA($@tH~3`?|<$T z^2F!`%pY*hvG~bSnHw%--g{)$F>)^Z zvuFG<+S*N9dui(+^^bPvYTiM;pLm>n@+5hh@@#_S)tB&IzM@Xl6!pRqMXjw?WS)NV zkL=NB9{^{TD3Q9|ip+tRIp&c=k2!hfk-919BJy_15A?{>Twk6Ht_F^7zLpc;a%uHd z^??gK__;jRM)o$e234!|0_@}7x>v3>I+kp`$tXJz&;$7T5>JI??7+@1CkAjH{6Kt_ z+)5c|N^0ca%k_ub{!4m3xtoKp;r?dgZpa5GMnE1SYY`d0O!40Hefe8GMJ}hb)fodm z=N@@yPCxNy`qH@i`^{CzxUL)~IS2G`nN19!SDfGa z7IExVgM__yxX{;568hb&#VbAtL^3{M9|1AYWkGC@_rc-%Uh#PSE@aq=?`KQjViH>< zqG`mrrO)93p|2YbJh5r;)#CHcO2L%5%6OXV!%gAik!0|in2ayl`PWHXCn{|nY3E4# z=!$XEj{AO8#((hJ1>v{)PzoN~nN#olfl?)C-2eVb@lT8RlkUX1$q^kc;av?4Ch*I2 zwa*?YViedAJ;!PX{b~C7-x0TjU$}g7vwX04mMUvxoP`C*n!xItc#I@S>}z;UJ%jVh7M%x`{z+N7TQ`Z*PCwa zdn5CZvG08+SZ|ysb)%kr)#u2h+&}h}Nj5S`Kc!59o}^4l5QqM6E0fx8{lYToM_b3* z&Y2fk@XLXKy)qLY`su=6fqas(DgQi4<4z1PnP|l?pIVP5$encM(~2)BpBkaHUiQbA zlut{)qT^@5ZRS@{Ukz@?4&%ThK-xu7GHkx?1W9D2%B7ydKSdV z=OqgIjm7v1?9>;q+gDX?WPZ2K^STnzSyhmFk#!^YO5 zeeyEUcP@UxPI57jOHF6RpdUqtXCx0fvdi%;blFH-fO$%vbI--!*L3OsY@Q9zoR#94 zIlXzNYtP41(Ui=4AeE2Nr>*FObXyJ`2k`56=a{8nA;$HE1#%GGe{$ZD9`II6zA=y7 zsbc$qhZ~iMZ+y(n$>2&O*f}#{V7LJyR`nOCmCF%uzo`y zzm?L*jn&TneJ2+!0q?r{Skmjfw{G39tHfdhQDFz7URyg%>@Y;1@lM@4SeS%t4@cN+c9VN5rX=joOEa~1tG{Cw=uB?(2bf4Yh@8t-IZ z+gFuj$3pfXKg53Fm5gy6V`Qy<{-eUaO*reM^uAo#x7_C&smmA1xo;}TKYpl0=G{p@ z&oIB&srx?h{o};YGi6_j++VE^VsF>oH!G2A>F00k3x0cqqD~s3Sd)Ulph8hEILmEw z_L*|;^Dt)*@sO2DWX?^j`KBsed+weaHdlH!Kf~Vhok^RVjRw5nrZB{7O?~rrPOcCmnbwYi5F+PKKVl^+Y-|YkP zT@K_JWlFC49Q65YzObHwMt4>7&U*4?`jxO}yVwX%FE+}qFIJ8EY?&KqdiDMimMZ=P zcOQ8yK;FHavkM2gH+slQ2M!V@?2i`qx}b!O!jm+zSEZPCxV9HlUzSUBRVN=HmSx1* zBmSWCEQ_{ISkkw;b;k+XIRQTowVRr}Qf64`{jB6{^^_MK1M40oE*gD zK1Pnxhf|LJ#$1}=)l>Nn=@|Ihkdte)NuDDi-*xg2-S;0IpO4JcYS-FY+>>haIqLU{ zp{MF^?5ZzqI=B8WoUu6Vw=H$^qRHoI#MXA7@6e^ZBZl97Pi&Vl{T}6QdP3TXJ?pjo zi_9Bwb9K^f#UGaWb287TyKeO7QeHY|?RE7j{~b>{7ykLBEp zZ*6TU0w$>C}SKF{<3Kox~?)`*&DOihN z%5`>+{#u;2G}%^q4B~!gj?BHg&$j!VarJ^@{9bwPp-nzriryx={q4Tf)+VRMApWHr zhu9Mi9K^cxy;S+`6uuiB+9prH$-|OxPUmPq?`DBYZjmy~T#XOzzG1iOYpStH>fC$> z4Sc5aZ|=Ktm9445_U%^fW^`u(GGD`gwQL9Wb(^GrZ^z0*pQ^2UI?3HRVQmD*>AHun zl>Dxm=t;>__#@TyGw7}`HO`ML&J{!sXzqLMoVC`jrQdUMgzvi*KMlF>0epxnyu!RI zTgr)S@;Ce2GN(_pafJ`$xgo=2(z%@BS$L#!4 z>^b~uiP~z5Sck41?X(l#>W;zM30>xyonkB4*av7u``yAA=(M257`bm zJaQhX{GAEux@v19adi5GUS9%$IQhdwK18*Cy9a&m`CR zzKj0o+-HvIc2e{@jWdvflJ+f`dn+8d$R1i{*iMZ#%mDJb@5?<%-rg`(=Jq`Q+*6Lc z;C=U9(GTBrwp%|fx7%{%+~CvHJJA`sqMRTXhjnp1xdpc)ug_47@EG<*b2gI1@oUg6 z@;t?Zz$NZCFx=i2ds zalWCVcxJ`P-{TCB6ik%V^{|{bYeA_aF1Ka76$*2SV#}<@ws{yhv;zG;iVdAb;l5&l`^+X{5MD-mM!%rV^zbP z%MhN_Rkvftd&ljMkE(W~@0h;74*5_2bs0g~UW?!iU(TY_zxRo%zoIF6M#eFF|88Lp z@eBK4MvnW=Az>);y{O0jC(5#-0@ZYp)SvM`~qWP}gYx|42q!k`OXE^j9 z_n22B^FH2~mZhBKCV3?NJnD1C5n7K-eOR{FH5T0$6REYx96f{aAmfb?V<~!=F}0`0 zr0_rE2{WED+9*Fj8)=yz{p1U@J@4PwcKm~XyX~{D{`a-r_UDuh@hW|=He_Cv5;paU zqwC7hYf{&xUvB?4dZmEsv`(mwUrW-)iKTS6CUW zIWn+GAILavKVFEPcvN&0>?ao0R!lBEdo8r$NXBY%qC}4F7o1n$PuL!8i$-Vw+tzIQ zV?eb$!i=gO-RjSNs7s^Ch8a93ci*ErIo8P5Onu=;x#l$=dCi$>hV@u~q5iH&Sic)9 zbp0jHxk?KAqmNYmM<-O2vg0xQ6((}L2D?w!?4@6K+U{6Qp1g>w zx>a;?pKGvP(FeLU_T*=G_>LqK?4z5CEPi({dF9cOwpJzV4|jKRUbRRrn-aA@9uIvp zxA;={2isTfB>+Zxng6(c?m%Fqu{Y(i6TpLWUCAZ?gx?WZ_D5myo-^+U{m1Pm28xJ( zAabAc{#Rp*?k@#5in7z+$$78^d3NKm+$4Jv=wD5GE~nqZnEufO5jpXauwNqH|Ite# zX+5Us_ozxz{So)LCX)P)h&sevo)OTOd%%MT+RNDo@%Clta{m8GnXsj=@*~ebclgv# z^&EbmpfZOuFfJs=`xe$9zZ3S8Cpzsn<_i1O?~CMLUsdc$ot^rX^PFL;A@+Fh z#cxd>-D~-Z&E9eSn>*NRgH9V!E5--mmEiHttM>d!J-3zm$jOJ=n^OF```&+v!!Hvm>368=FOB@oM%AvtNXE z<-XOv?aI2?I35-Lv^bX=8^?p)tDuKD*D%JuB<$c3?VLAK&-w2$sdrUrXTe9pKK4Ye zeMcF;Apw01G=z0OHo-PWwf2GA>ytmM1!s%RUqxL;c4ipX?X* z{$(QhF+S18L&Vd3O47$Y_F~BOorH78U-!Oim7xEzi`ACh%+sCommaIM72pAUPam*0 zb=$+9kT!&RHn2knm_c94zSuio%CkafZxHma<&q`X@V6q)YcO-nN`|7^{ zm*cE|Jgk3+uOsKmkaOWNL59HnO2K407PCXC;hI`0as1}i%Ip1wB z?=|FehT-|pRvvqd$@OBt7wb`(=PB2#By@kp5WvfqZ@>Kub}ob;C@2g zSB@PQP^{MT@IBM^vQ3PGJx`_QFkf`$K;}KD9URA?cjUlejWf#Fm%V$AqF2i`5%XM} zP$J}V+Bf5aZDN1(>f(>+pR@IMu@2Du5=JU}wyg37XNRxni?~8~%;+z+HgY zSRZ(lJk<@4A)1e|0=M&0<6)iNU#?RbPwiKgNcDX#474q@*95O;iHT!{ZLELgOm@sKSpb{HVf@ z9{AB?alSwN=z$*<6*__+73j_@`SHurA2tel_hIM|S&#oCy$-iZ63}HxL|BiE5A>5h z3g-E9k3MxOzXO%vOwC4N{?_N0d1uzQcM~`I6=$Bp6UZ~y*O-M(z}j4{*YVxBc0hDE zFvPzuT4DYv&QF%Ec+q3iqO^6LyqjWn^}MTZjay0^X1qILL%Pb=UY{kZDtgKl{C(g*7_d_IEltpd+K zmG1{-8%FT~#*OTLZ96!_?*QI*W;32upLIr#KgxK1jUIy6ThSMC534-OqxX2&<1Fu; z3eRnUuKBLhyjCmtcHVXI?a(@Tj8ad@^?yWb?$%Y`PwA>3m1s`iQdjlqJEg0>FmLpt z8ObA#9vF5U8akz~s*4v^Xy9AY{tk~7Cf1edt`oZ8i*7ct8|8SU-D5U-J*x3!Pr6p% z-(G$s`t&$-%NU^hQ$J7famv^6E!}sO=B_*TT*3NB?ED^hTb40Q_hWx8;s51NaK^f; zGu%40*TdJWJzijJA#kmrhYp~RK0q%i=%0S7?&DfP9o9?+dLO;Rda7Pod$_ZcyqSug zu#vxW=lG?3@qcoC{#5Z>N0+_S@RU_EOLF zx|%%4wmzDrF9S|F(w^MY85v)w>X)JOb1w5FbKZ#Q_mZ=DU|C7H8VQJ-TU+;hW-0p?(H4;3x9ywhpi`blTYt;^vvKtwY3N0Yw2@HF-z>rAO1Q)|(UX&P|oLfTWnRKiM*z2ohj zN3c2nJ|0@ZugLzOpL<;Vh%)y3qBm>NCCFzdHr0H)ogw2IKZjmFL5K6(O53qZu?Q$ceyEW@o9p2HsjBzvVUBPeB^gZL^|ftN>%Q&NaTOP4pnX}8<{D8+ioG8>2HRNyY!GY|{Q8yIk;FAIbKv$I;wt@;SvM+n` zvAvBPID(z!1xBmk_YEFsPeVRw7I?(I(6BGO*cV>x3orJC*Ba5TVqd7(7b-GTg${g< zjp1v9o}ghzR>a?1`<5d!e%uuwbMA{jxpbL|nY!;vnfs9u?#CGv^_=5zd(a^F0zUxf z1joqJAXlSoI_Eu;uTfmjIncM08^4}2XM56;Jcp)Fo|HVpM$!^z4@)|7&L0jw6}G-f zzFwM!^3P{&Mh;!Uwd4qILjNuXj(ei=EHi2UTW6EIa!!f!yL{@owd8zEA-|$ds1p}M zD~-ZJ{)Tg|IcEY#$Wu=qdWp7n)7DU5-<$fKHS*_YzTxJG z)YXyKRYh)9JA8V8-&TCVYlJ@VA=iEKw*z&JGxB}$bd2@SHaup2d}pDaE!UX4!ZKG; zFCc%Kn4{bSBJ;UznL~|FIpzC#x9jXBjdP;XxzV)k=BuaUuF2KWCr%F7J_V)Ta&ie%owt{}1$&7yaW!{~$xp zz_uBKZ4<+`xft7KI{uvNu_@~D=iDxTKM#AF^)oukC|>W-xV0(a_->4HWS)!z6O)ks zB=lA~=I~o}WQ=t|y`t}3!P)-g)a_d#l0JUt0Kc#iAICWrdpM_pxLbWIZQKqWW?rF| zu-@=#SLm!wllUL33T%a}(ZD*6EQ7X)Q_JKwr-tBn*(c^0Q%CH#-T&`ZavdWr*E+t<< zXCGb1qHQN6f7_g$W_q-*qhA!6i=nd5)XBxrf=(`mTtj15C=u<>sI84w%wrp)I%izj zoH3{msF5~ZG?~3kiFnZ=0ou@>3P-e`g0rZSlE-m7fvjeof`5#&2y6Il(gyrHyRf-` z8I@<2m^ZfmQ$+i<(DyYd_Pzk;E;M1^H}G4r$o5z8jlH-^(O=3|?3Y#vopV48F-2(+ z2Z=-OB@VfpwSPb{Jk6rTldl-6rnIO>Id7RZ8Z^c5ZdY2oi-n=pi56{&FnqO23wFGb zv70kxb~CQq<+$*X)}-b*pFPnVCq%ND!(PU-m+|bQt*K3{De(PX)+7w^ZTf$f{(nvX z5AoY@4Z`qzLuv8wyAtYOM2q?d@O=htD&$dmRZ~nFBWcA>tQL{Xk&ORL7aqmCID_($ zsQKxZvF`jEn7^0#H_&D+{V=B<^OS8`)RE^LMYQsmIk$OqL>q~Xn~qnDYm(ZX^!+e6 z^SW(+m}+|}G&&U;y=(<=k!_dsf}w;1IqgbEj<(Ml>GgCpW$)ARjjP4G(7X4s?tl&( zwJHHHW z^)Go29R{*R$C2G9q@4^6dXm0pS%00~Pvv*(92)cm`Z@VP_!#ix5I=%;sbe=|^Cxyk zE#Mo;{xv-FEVkfXQS;aq&dWky_25T+1vKit8-A>UA9ulz+u+9y4t}g+ayv`NvrPcY zYnlTQjeO27SVpIG!!kQFk^oO9wge(szv9f973{;$1fFSFjsTXwhaR4W9=15Vu1dRp zcy=bgC(Aic%~5l131AfH*Rg~*HgsH+{vm)B+6 zEOOd$`#NF=@!^2`p0c3h{f+-8)h@K%)&G3?PFglHk6+7oQg+6(k|((LoLf(%d=>oG zMY@hNru5w7qPtyjZI3qhr20+4B|2NS>pXYsKP{i144bx@a!%=YUDynJrgvNM8QtyN z`;5e+#c#r?;d<6+_2d^J%kL-WcEcWNdw5m(yJ^L&wT83S8qQj)oV8XtYc0~Ktr6`W z>`M>!rAHm9@SDKx(#NYw-Rt5(boRBoq^;GRKNL;n4@Fr+*1!|>@JAP~V;f>4ewq9s zca8V+zI_^H-L@XS0LR`NU-CHmDF1Ppt8XsZ>%>)cEean-1NQ21tSe?hvuk9G%q!!r zdO-R_TzsYTzGUtSd5RwU&eZ!W#?fXb^orkJuFHj!W5anWZaHlqn1eII|9bc%)@36Ck523b zY&H!U2>j2MJSF`+>ji#d=G8b?|KwwQK!+TElH=QXAP(q?h!WCP>fRLi~(U=UEm z&b#QRsp&W4Ph-qk=l|5s;!MmT{2zSgr*^O%p8@n+8RLo#L`=}AAcj>m9UGr(_;%v= z*JI-V7kF99l=Rv_o-HVC5*c&sxF_>d#{Grqp7}ZS?#AIPIp0ofZ^gW7pFiolkGS?Y zk<8djzS>cbj$Q7YkGC8g{ML`}?Gtj2F6SP$?DB!c05VQUza0DRqWbstblS(N@h82* z*`$7-d161dIQ7c>@Mt0a!*i&2uIPAoKXncY{f2SiVh?ehL(oi|Tulvp?&i1s4zkw; zdh&hTX~Yk~U((-}PhXtFmc|V+b=wnVO{s{8)jwu?u+bLk>{aitnD?k&eWAg5da6&Y z1fO?I5cbk@MC8`|n0fp~U(zFD_KBs6YFtDd{2~$gPLAR{ANGgZo$>5n<@8u6iuHE}vV5#*6g4#2y*61hZGB7PA|zJu>|$#NyRHbY6iGmE)> zh_3F2f5&@=&~5j==g`31eZDjeL?jJ>3%NFvG+>BdxN9hIFV94jIJa-u(liSnFRn)D6KLif(J^QLi7tG9dI%We4*|atw;h9hCnUe6@!#>?gD22| zDLGt^{k}bF9^Mm7`ghAUq7qpKjm+5Rw?6@|9~1ZX9FCet3e?E*-Hh{Cr*4tYFcDhK z`KYrccOm%~+ym|%hwzbSTn24_9JN1syi;E|p4^N1(a0?~M9sl!p1J4^PcpF+THD>J zH?qEa5FYdF=`<<}$b**mJfdUfcTOai`|x9DC_4Ec$;1;;^W*$%$rtL`(hq+OfB6%f z7m55~9olhdugsAN{HOERYQO*g27jr=jz08Ad?a&bMGYUcszR&ybIgK;eo1$FF1X5V zmvmaFr}>cen9GOtN*5olQ+*B}_E-ICWxdji4?lg;|L^C+sLy(ie3j>h!t?xgsqX{D zT&;4pyW|OxcM^|!)MF>}Xz%fKy!YbAy?E50;?ay#@hJ9EUmo?RdGx@)!J`M@QS6{E z%cJiedK(+?cFc@tc{p9uacyS#n;dqUT=7?@1m} z%p2bHCUfRTlesga`p35@$y}sn}4|g$M0p$ftormB`c^*sE2*KCRuzfP0@b zh0h~(ztjijJ%?PGfi94170y6))-#e;BZ)&%c}?Y(JnCL2CPkl*(xdDZA9N0*e1y0$ zVJzVxXQeh(c3)G)x%`8Po%(pi`TVD`HXzkVDAc%tYaV4nzD{{P*NKz|q{`25eZmrA zD=_nja`74E%XmI0^_)}`A!Z6u#l<81VmjrE&;>N+w6~J$>o*ELGvLRv9|{rT>Jed41rNMm=l1U**W+r+$7V1_r+rPVr+hm76H;{Ae}?j5zL-N! z`4j#h-Q#%?{|{j75MMzm7MbX0m^fEo`&BGl%FT|D(CGHMPeMV_yNydka)yZ{IisDL8`qb%6If= ze;egRO?;jHM~GvTU*UOr{08!B^|b;>%l36~DgUgaJlZJ_5VumE2D~Nypk9H=BNjm) zWt8Q7->3ZU93dt+&j*P?Je$mT<4*YuaRKETzOp@sKKOqf-|AF(J^xF5xF1bypj<=$ zleys${l)8){dGdT?Ua3@1lp5j_u~^~lqG)MPlgyFX>(?}>=%`k8^8g>bo#%Xauv^A zcw~xosj^!mQ`}4WL7s2phDQX%r>V00JRm-!JZ+W`n5;a{5|t3XEW4j9F^2L=`b)$2 zTa^6^fQLh`L&Z|cOL$*CCk;_NmnysUgW^TXxAEMipKLK2-(|lD^!JF5aU&A9~^y-Yyn{u<<0l zxt{@I2<2xePjjBoO&@UmMj6x%45b(Nud7)ieGeu%A6exk>CDk;nN-A{oSL-|;b z=hG<5_T5jRxKfsTJm)A}XFl%pfnpV9N$>7wkhqO<=sIu?PkF>(@taiHeLh(Hmhx8k zTf)a9P7^FuowEDyFuTVbBbC+ID7Y(Vh`~K--nS3Amaz8^w z6XoR)cZDPWhKa{gW%v0ov4`?D@;cUW!y`f>yPsQjpNB*qWy!zp=M14!eiUMv$_mS;~~G)bqjpUHR`mFA=9xmh#^Hghhz*x$!jrmWm&w%I@=0@gvH|dHxDFJV>#} zQ)TygnRtq_)FZ+E*?m4zTt~T(`5_)0_^qWZ^@aPX6n9aUGH_t(`7X**-?$%-c$~7-C+YH2 zl%@Vim!G99^+me;JY}gb(&d*ZOMQ_p@1y(xWysbUj}-i}oPMrS-a(n=`~TD4wZ%$O zl;Oz3UV46>OdU;L@TKKw7hS0Wr{veNH75DEM6^MT3UdVC@9?ZAHge_w-r z_?5s%0(rv*`|vR^**lM?!1#;@BX6Dlmm2KD9|Fel8D=7N_|F^c!+!}(_SfSlfN?B4 z$4>&2{jIN!-fw`(-g*2x;PV3fd*BOzeg8Nc871ub=Kz!a@%S~sWKTT40JxezR)5&& zT|}5B0}Z|enEIc`ZvZBHDhrVvm?+!+lAci7?asDI=2N3PnoSMS{FK3d3w z*BtC;{eFMCvpv&1ZQlB7I2`M~oleKo{$VvTLpxoObMSgu(Xy#J+CO47k8e~*Z?XDA zqmi9%UU&26m75>Dd6ThP&;Ft5_$ZW>Qp8FqW^!(1EDNvfLce9XjMo~}>OcfnF_&ec zq|kA$imZc(Osk2~gW2)^_=uglVWo<38Mm4#CzUaItUAak>MT<3Zr;4A&S6eD>yJkk zs%ci_rOT3>JDuiPzFuT|sb#2qJ3V4KGvj{q-bXz&m4TFUS8|nW7uz&3BC|dvG!UW3 zQ=7Q0nY?v6nAzee{q})L!GtHN)h@GX&UGPHqD^K~!Oe6$=?_Nz$xvxanXM{1ineFd z!RSyF-BdlR>ZRt#gh_c`$c!72%!?74$h^OM>>VC&s_jjBkit;}4Wp=1nIt!ABT1;8 z#X5DR=CrwBBu^Nh7i(xss`zqyIM7xOM#dgvX$8={0s2l2hNvK3Fl9@v zg-oTA@w|QQQ&NuJTB({%cSf_p7MX-BEedqDBxjt*GD&>hp9aw;Q!2OI6>%K%G%io0 z=r|QJGg+z7Be*Sh7rm$&mRDKtkH$NrqbOZ(amq5ui$aQ|NXks^p8tg;SqVbfiL@+@ z%2UmZV?3Q}5gNJTn(=f?&1QC*Oq4++RAVAx6=SIcD|qTlg(C5)7jNEp4a|4D{aNn1 z$~kQuPm4qeV+#{Xt8GF#9uIdDHrR#uZ^YXytcHHsda;6q>C+8YK$pWo}p*7j+9m3P3w6OOVFyOYEt!BOZBYExU@Eo zg@^%lYi^R!&Q>zmn#j6r@m5z=V8^d+%9NUeYb^TC_Tb|-s5K##EAlKY(SLI-=IwB$ z52xeFY%m_JDw`!D!QjPRQW%ztMKjvnP%;>i?ODIHhcFV?T`I8W)ricP9uJ3BQ<~ed zoK3A2qp`uXfgh-5ZNcapFr%oVMiFtN=z9F#Xk91nmYQPv8ATu2nPL8OE$&-zH`9aN zlkm|#gw`}2sUel^e&Stw3&oJ=O#>QFOA})fm^+1uuM-Ii8ge}+{a-DZL~B`?RHPZT zGcA3=gblUJtbP6bGtFFy8o;lxC3E~#Q(9ghI84e_$*q-G%E&yl-bE44Cfgd@mYPJ- z`)@`!_=uWNd-e^qT8gY1>G5PcifFPMMOWjO+L$1SCdN#PoF%5PE=h%PK7Tah#JXx` zo8O!0J+&i^PDEynv6wRvJA)y=Y@bM+Tmfv|nxb%diospd!a`$`;x*ZCoi3NODDs4K zwYoGCrrpaWb1v5oMc0jw+UWs%EU=PNqA!3JgHe_u1GM7 z^1S2%>tJ8#q}kS_$w5P3ASB$2wvTVkREg(dtcmKk`q`^D1foLnIJQ<{GAuQg8<*)r zI}z(q@vq-9*gV$5zvwzO8jqsFBr4CX5L}mu;b}^fH?7gMHP*M>L@e!2%{o(gOv>)F zdv(;9R|T=u^}22g)M+^8T+W=733_BMFy}^RhkZ~vXGtd0h}HW4e`r2wXhxO(mn}Cn zs{iRqS)CTli^T8Vb=YM)^_|0xeUxFtqAOK)r($=amYOU3{t-3Pj= zZkNWQQi#y;sMe_pPGRTFN8Qg_xXuaou(aW^*1BYwHO?jYOP*m{j?27A zbyStxQbzXJEShVwk1d3bn#^0pTAfk)m{{#)JNMcUMHWtOC++72%X9nHQ?BLH$2OX8Xvyg~J!y*Uj14VSb3~oIj0D!5 z2>2vU^P$P2>ei3sUWGH5OFH*}N(-$@Z11T;=UCV|C(%zxrX8KXnoL;f3@>HjoMnnT)6|%s zWUgZ;EU4V|veTwD_`H!xT5PB9dOFe6(&;#wfT%@bs2Cd+!pY1g!o|!o#-%MqUL>hv ztZXd&X*zYNQ*O`s(n&zuK|)goe$uxnwWYZ0PZ3Tkg0*HsbY2=Hp1HPIS=eg0)Yy#` zDYF^ZLKjZwHrJ)a9$B1bWg5jnjbU|CG8G%Abz1Tg`#CrpVXz%}f-}S{jkpNdMp~8# zV{jgUuPLO9VQXT=v93w6d!uA6x-Qw1$MN<##QFhvbOOB>bM-G8QWuq% zg|2RC)(W1k{Y*$}Y{%Mqt%in%tjkmjky=)knaU)#m4(W&1*u|DIH7q2vRVG;jnulW zxGHW}GIpqtQfD?=@`_&bzcf&j2|L#~Rx ztK_QWyOJ>wch|XUo?RtZ?W-%fGVQfJ+lM|?lj)j7)%iP;>%QNWyq}!bOWb`m6^5p= zpQgq3(^^>7TKe_pk2p-B;}bey!4Xk&&f-s4FwX38-h&9OXGMF*+U=BR_gH(L56!<3 zUH<}C3w#@o?^_#c_4z9Op}G7keP&JX^esF2OgxGE(fPOe_~y6a*S-L~9(|ThzK@^5 z_o=~WtaILb5l-@mN8hkM)O^PZSY2M;hk-ngeB17^wSP=Qo6Gw`z$2fw2XQ~`NqBmB z&jvj5bvxl|e>_k1yvvZNPnUe)9>6U;DUi1l@W>bLaon>O-s1s}eB$17sP?PzRG%(K zMfh~d7w$RSG#KgM?P`qeyly+YarOCOTKas;%-a#I{}Y;=6;L&2Cp2RUcVOt9{I|V zzuMU?yz~`|bSocC@aQ{le|dQ)0v`Fs-3}i4&-u&qo(g!B=3~3?(pN6hC11JMZMDn$ ze83|gz!`XL`FJYekuTj{;N@O9JiUIGylRmy`Pe;*oBZtj2-LEI<2Q3~?m0v`G1y>X}Z&s)yN*8(2-=p8r) znVuBL`)9x-U%jVspJ?F;*fOsl`Q(}7D8H8e@#TO=zIo5#zNkgsp93EG=$-Y6+KP5Qbt33ga{I&h%d6%EF;E@mCV~yYTGVj2-3*Lbf@Snl`s3+m+<$W*Uy$H_z zpRE1-Jk|5?pXZvF3;FsT{#5NhzRdeVz#|{P+wO+17uv4Byu6OT!F%pO8}uYRJ@2Ohk9-EtzZbr$ExdQUc9Aam3O$D#O=y03 zc_QGE&& self.sys_transfer(args, memory, host, metering), SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), SYSCALL_COMMIT_STATE => self.sys_commit_state(args, memory, metering), + SYSCALL_CREATE_ACCOUNT => self.sys_create_account(args, memory, metering), _ => { panic!("Unknown syscall: {}", call_id); } @@ -809,4 +810,44 @@ impl DefaultSyscallHandler { println!("commit state called (ptr=0x{:08x}, len={})", ptr, len); 0 } + + fn sys_create_account( + &mut self, + args: [u32; 6], + memory: Memory, + metering: &mut dyn Metering, + ) -> u32 { + let addr_ptr = args[0] as usize; + let code_ptr = args[1] as usize; + let code_len = args[2] as usize; + + let total_len = ADDRESS_LEN.saturating_add(code_len); + if matches!( + metering.on_syscall_data(SYSCALL_CREATE_ACCOUNT, total_len), + MeterResult::Halt + ) { + panic!("Metering halted SYSCALL_CREATE_ACCOUNT"); + } + + let borrowed = memory.as_ref(); + let address_slice = match borrowed.mem_slice(addr_ptr, addr_ptr + ADDRESS_LEN) { + Some(r) => r, + None => return 1, + }; + let mut addr_bytes = [0u8; ADDRESS_LEN]; + addr_bytes.copy_from_slice(address_slice.as_ref()); + let address = Address(addr_bytes); + + let code_slice = match borrowed.mem_slice(code_ptr, code_ptr + code_len) { + Some(r) => r, + None => return 1, + }; + let code = code_slice.to_vec(); + + let mut state = self.state.borrow_mut(); + let account = state.get_account_mut(&address); + account.code = code; + account.is_contract = !account.code.is_empty(); + 0 + } } diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index b992162..61b8cea 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -13,6 +13,7 @@ types = { path = "../types" } # adjust path as needed compiler = { path = "../compiler" } # adjust path as needed state = { path = "../state" } bootloader = { path = "../bootloader" } +kernel = { path = "../kernel" } once_cell = "1.19.0" serde_json = "1.0" diff --git a/crates/examples/tests/common/utils.rs b/crates/examples/tests/common/utils.rs index 662cf7b..d28fecc 100644 --- a/crates/examples/tests/common/utils.rs +++ b/crates/examples/tests/common/utils.rs @@ -1,4 +1,4 @@ -use program::Config; +use kernel::Config; use compiler::elf::parse_elf_from_bytes; use compiler::{EventAbi, EventParam, ParamType}; use serde_json::Value; diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml new file mode 100644 index 0000000..6092861 --- /dev/null +++ b/crates/kernel/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "kernel" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib.rs" + +[features] +default = [] +guest_kernel = [] + +[dependencies] +program = { path = "../program" } +types = { path = "../types" } + +[[bin]] +name = "kernel" +path = "src/main.rs" +required-features = ["guest_kernel"] diff --git a/crates/program/src/config.rs b/crates/kernel/src/config.rs similarity index 97% rename from crates/program/src/config.rs rename to crates/kernel/src/config.rs index e39b61b..6904ce6 100644 --- a/crates/program/src/config.rs +++ b/crates/kernel/src/config.rs @@ -1,3 +1,5 @@ +#![no_std] + pub struct Config; impl Config { diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs new file mode 100644 index 0000000..6020e0e --- /dev/null +++ b/crates/kernel/src/lib.rs @@ -0,0 +1,4 @@ +#![no_std] + +pub mod config; +pub use config::Config; diff --git a/crates/program/src/kernel.rs b/crates/kernel/src/main.rs similarity index 54% rename from crates/program/src/kernel.rs rename to crates/kernel/src/main.rs index 3a93dc1..b9b1763 100644 --- a/crates/program/src/kernel.rs +++ b/crates/kernel/src/main.rs @@ -2,38 +2,29 @@ #![no_main] extern crate alloc; + use alloc::format; use core::mem::forget; use core::slice; -use program::{log, logf, Config}; -use state::{Account, State}; +use kernel::Config; +use program::{log, logf}; use types::transaction::{Transaction, TransactionBundle, TransactionType}; -const SYSCALL_COMMIT_STATE: u32 = 11; +const SYSCALL_CREATE_ACCOUNT: u32 = 12; /// Kernel entrypoint. Receives a pointer/length pair to an encoded `TransactionBundle` -/// (produced by the bootloader) and walks each transaction. It also receives an -/// encoded state blob that it updates and commits back to the host. +/// (produced by the bootloader) and walks each transaction. State is managed via host +/// syscalls; no state blob is passed in. #[unsafe(no_mangle)] -pub extern "C" fn _start( - bundle_ptr: *const u8, - bundle_len: usize, - state_ptr: *const u8, - state_len: usize, -) { +pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) { // Copy args to locals before any syscalls (ecall clobbers a0). let bundle_ptr = bundle_ptr; let bundle_len = bundle_len; - let state_ptr = state_ptr; - let state_len = state_len; log!("kernel boot"); logf!("bundle_len=%d", bundle_len as u32); let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; - let encoded_state = unsafe { slice::from_raw_parts(state_ptr, state_len) }; - - let mut state = State::decode(encoded_state).unwrap_or_else(State::new); if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { let count = bundle.transactions.len(); @@ -41,7 +32,7 @@ pub extern "C" fn _start( for i in 0..count { logf!("processing tx %d/%d", (i + 1) as u32, count as u32); if let Some(tx) = bundle.transactions.get(i) { - execute_transaction(&mut state, tx); + execute_transaction(tx); } else { logf!("missing tx at index %d", i as u32); } @@ -52,22 +43,18 @@ pub extern "C" fn _start( log!("bundle decode failed"); } - let mut encoded_state = state.encode(); - let state_ptr = encoded_state.as_mut_ptr(); - let state_len = encoded_state.len() as u32; - forget(encoded_state); log!("finished bundle execution"); - finish(state_ptr as u32, state_len); + halt(); } -fn execute_transaction(state: &mut State, tx: &Transaction) { +fn execute_transaction(tx: &Transaction) { match tx.tx_type { - TransactionType::CreateAccount => create_account(state, tx), + TransactionType::CreateAccount => create_account(tx), _ => log!("executing transaction"), } } -fn create_account(state: &mut State, tx: &Transaction) { +fn create_account(tx: &Transaction) { let code_size = tx.data.len(); let is_contract = code_size > 0; @@ -86,40 +73,40 @@ fn create_account(state: &mut State, tx: &Transaction) { ); } - if state.accounts.contains_key(&tx.to) { - log!("account already exists"); - return; - } - - let code = tx.data.clone(); - - state.accounts.insert( - tx.to, - Account { - nonce: 0, - balance: 0, - code, - is_contract, - storage: Default::default(), - }, - ); - log!("account created"); -} + let addr_ptr = tx.to.0.as_ptr(); + let code_ptr = tx.data.as_ptr(); + let code_len = tx.data.len(); -#[inline(never)] -fn finish(state_ptr: u32, state_len: u32) -> ! { - // Persist state back to the host, then halt. #[cfg(target_arch = "riscv32")] unsafe { + let mut result: u32; core::arch::asm!( - "li a7, {commit}", + "li a7, {create}", + "mv a1, {addr}", + "mv a2, {code_ptr}", + "mv a3, {code_len}", "ecall", - in("a1") state_ptr, - in("a2") state_len, - commit = const SYSCALL_COMMIT_STATE, + "mv {out}, a0", + create = const SYSCALL_CREATE_ACCOUNT, + addr = in(reg) addr_ptr, + code_ptr = in(reg) code_ptr, + code_len = in(reg) code_len, + out = lateout(reg) result, ); + if result == 0 { + log!("account created via syscall"); + } else { + log!("account creation failed via syscall"); + } } + #[cfg(not(target_arch = "riscv32"))] + { + log!("(host) account creation syscall skipped (not riscv32)"); + } +} +#[inline(never)] +fn halt() -> ! { unsafe { core::arch::asm!("ebreak") }; loop {} } diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml index eeb8bf1..7c45774 100644 --- a/crates/program/Cargo.toml +++ b/crates/program/Cargo.toml @@ -6,13 +6,7 @@ edition = "2024" [features] default = ["guest_handlers"] guest_handlers = [] -guest_kernel = [] [dependencies] types = { path = "../types" } state = { path = "../state", default-features = false } - -[[bin]] -name = "kernel" -path = "src/kernel.rs" -required-features = ["guest_kernel"] diff --git a/crates/program/src/lib.rs b/crates/program/src/lib.rs index 509ae85..875e1d1 100644 --- a/crates/program/src/lib.rs +++ b/crates/program/src/lib.rs @@ -16,9 +16,6 @@ pub extern crate types; pub mod integers; pub use integers::*; -pub mod config; -pub use config::Config; - pub mod transfer; pub use transfer::transfer; pub use transfer::balance; diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index ed913f1..f335718 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -15,6 +15,7 @@ pub const SYSCALL_DEALLOC: u32 = 8; pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; pub const SYSCALL_COMMIT_STATE: u32 = 11; +pub const SYSCALL_CREATE_ACCOUNT: u32 = 12; /// Trait implemented by syscall handlers consumed by the VM. pub trait SyscallHandler: std::fmt::Debug { From a82141923c53967a1c18153f86de7b47cdf4103f Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 11:59:00 +0200 Subject: [PATCH 15/70] [WIP] memory tables --- crates/bootloader/src/bootloader.rs | 32 +- crates/bootloader/src/memory/memory.rs | 414 ++++++++++++++++++ crates/bootloader/src/memory/memory_page.rs | 332 -------------- crates/bootloader/src/memory/mod.rs | 8 +- crates/bootloader/src/memory/pte.rs | 33 ++ .../bootloader/src/memory/stacked_memory.rs | 55 --- crates/bootloader/src/syscalls.rs | 168 +++---- crates/bootloader/tests/allocator_test.rs | 12 +- crates/bootloader/tests/memory_page_offset.rs | 84 ---- crates/examples/tests/common/test_runner.rs | 10 +- crates/vm/src/cpu.rs | 15 +- crates/vm/src/exe.rs | 41 +- crates/vm/src/memory.rs | 93 +++- crates/vm/src/vm.rs | 10 +- 14 files changed, 682 insertions(+), 625 deletions(-) create mode 100644 crates/bootloader/src/memory/memory.rs delete mode 100644 crates/bootloader/src/memory/memory_page.rs create mode 100644 crates/bootloader/src/memory/pte.rs delete mode 100644 crates/bootloader/src/memory/stacked_memory.rs delete mode 100644 crates/bootloader/tests/memory_page_offset.rs diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index cc1efad..073e02a 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -7,10 +7,10 @@ use goblin::elf::Elf; use types::transaction::TransactionBundle; use crate::DefaultSyscallHandler; -use crate::memory::StackedMemory; +use crate::memory::Memory; use state::State; use vm::host_interface::NoopHost; -use vm::memory::{HEAP_PTR_OFFSET, Memory}; +use vm::memory::{HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; use vm::registers::Register; use vm::vm::VM; @@ -33,19 +33,19 @@ impl Default for BootConfig { #[derive(Debug)] pub struct Bootloader { pub config: BootConfig, - memory: StackedMemory, + memory: Rc, } impl Bootloader { - pub fn new(max_pages: usize, page_size: usize) -> Self { + pub fn new(total_size_bytes: usize) -> Self { Self { config: BootConfig::default(), - memory: StackedMemory::new(max_pages, page_size), + memory: Rc::new(Memory::new(total_size_bytes, PAGE_SIZE)), } } /// Load an ELF kernel image into a fresh page and return its entry point + backing memory. - pub fn load_kernel(&mut self, elf_bytes: &[u8]) -> (u32, Memory) { + pub fn load_kernel(&mut self, elf_bytes: &[u8]) -> (u32, MmuRef) { let elf = parse_elf_from_bytes(elf_bytes).expect("failed to parse kernel ELF"); let entry_point = Elf::parse(elf_bytes) .expect("failed to parse entry point") @@ -60,12 +60,11 @@ impl Bootloader { let image_end = core::cmp::max(code_end, ro_end); let image_size = image_end.checked_sub(min_base).expect("invalid image size"); - let page = self.memory.new_page(); assert!( - image_end <= page.size(), - "ELF image does not fit in a single page (need {}, have {})", + image_end <= self.memory.size(), + "ELF image does not fit in mapped memory (need {}, have {})", image_end, - page.size() + self.memory.size() ); // Flatten code + rodata into a single buffer and write once to set heap pointer properly. @@ -77,8 +76,9 @@ impl Bootloader { image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); } - page.write_code(min_base, &image); - (entry_point, page) + self.memory + .write_code(VirtualAddress(min_base as u32), &image); + (entry_point, self.memory.clone() as MmuRef) } /// Execute a transaction bundle by delegating to the kernel. This mirrors the @@ -108,13 +108,17 @@ impl Bootloader { vm.set_reg_u32(Register::A1, encoded.len() as u32); // Keep heap aligned after our write. vm.memory - .set_next_heap((addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32); + .set_next_heap(VirtualAddress( + (addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32, + )); } fn place_state(&mut self, vm: &mut VM, state: &[u8]) { let addr = vm.set_reg_to_data(Register::A2, state); vm.set_reg_u32(Register::A3, state.len() as u32); vm.memory - .set_next_heap((addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32); + .set_next_heap(VirtualAddress( + (addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32, + )); } } diff --git a/crates/bootloader/src/memory/memory.rs b/crates/bootloader/src/memory/memory.rs new file mode 100644 index 0000000..a36b6d1 --- /dev/null +++ b/crates/bootloader/src/memory/memory.rs @@ -0,0 +1,414 @@ +use std::cell::{Cell, Ref, RefCell}; +use std::rc::Rc; + +use vm::memory::{Mmu, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SIZE, PAGE_SHIFT}; +use vm::metering::{MeterResult, Metering, MemoryAccessKind}; + +use crate::memory::pte::Pte; + +#[derive(Clone, Copy, Debug)] +pub struct Perms { + /// Allow read access. + pub read: bool, + /// Allow write access. + pub write: bool, + /// Allow instruction fetch/execute. + pub exec: bool, + /// Mark page as user-accessible (false = kernel/supervisor-only). + pub user: bool, +} + +impl Perms { + pub fn rwx_kernel() -> Self { + Self { + read: true, + write: true, + exec: true, + user: false, + } + } + + pub fn rw_kernel() -> Self { + Self { + read: true, + write: true, + exec: false, + user: false, + } + } +} + +/// Software Sv32 MMU backed by a contiguous physical buffer. +/// +/// Design at a glance: +/// - Physical memory is a single `Vec` (`backing`). Frames are 4 KiB slices into it. +/// - Virtual→physical is resolved with Sv32-style page tables: L1 root (VPN1) and L2 (VPN0). +/// - Page tables themselves live in host memory (`root` + `l2_tables`) and store simple PTEs(page table entry). +/// - A bump frame allocator hands out PPNs (physical page numbers) sequentially from the backing; no free list yet. +/// - Mapping APIs (`map_page`/`map_range`) allocate tables/frames and set R/W/X/U bits. +/// - `translate` walks VPN1→VPN0, checks permissions against the access kind, and returns a byte +/// offset into the backing. All loads/stores go through this path. +/// - A guest heap bump pointer (`next_heap`) drives simple allocations; writes copy into backing +/// via translation to respect page boundaries. +/// +/// Limitations/assumptions: +/// - No unmap or reuse of frames yet; the allocator only grows. +/// - No access/dirty bits; permissions are R/W/X/U/V only. +/// - `mem_slice` only returns contiguous slices when the mapped physical pages are contiguous. +/// - Identity mapping is not assumed; everything uses page tables even for kernel. +#[derive(Debug)] +pub struct Memory { + /// Page size in bytes (Sv32: 4 KiB). + page_size: usize, + /// Total number of physical frames available. + total_pages: usize, + /// Contiguous physical backing store. + backing: Rc>>, + /// Root L1 table (VPN1 index). + root: RefCell>, + /// Pool of L2 tables (VPN0 index). + l2_tables: RefCell>>, + /// Bump allocator for guest heap pointer. + next_heap: Cell, + /// Next free physical frame index for frame allocation. + next_free_frame: Cell, +} + +impl Memory { + pub fn new(total_size_bytes: usize, page_size: usize) -> Self { + assert!(page_size != 0, "page_size must be > 0"); + assert!(total_size_bytes != 0, "total_size_bytes must be > 0"); + + let total_pages = (total_size_bytes + (page_size - 1)) / page_size; + let total = total_pages + .checked_mul(page_size) + .expect("physical memory size overflow"); + Self { + page_size, + total_pages, + backing: Rc::new(RefCell::new(vec![0u8; total])), + root: RefCell::new(Box::new([Pte::default(); 1024])), + l2_tables: RefCell::new(Vec::new()), + next_heap: Cell::new(VirtualAddress(0)), + next_free_frame: Cell::new(0), + } + } + + fn total_size(&self) -> usize { + self.backing.borrow().len() + } + + /// Allocate a physical frame (4 KiB) and return its page number, or None if out of frames. + fn allocate_frame(&self) -> Option { + let frame = self.next_free_frame.get(); + if frame >= self.total_pages { + return None; + } + self.next_free_frame.set(frame + 1); + Some(frame) + } + + /// Allocate a fresh L2 table and return its index in the L2 pool. + fn allocate_l2(&self) -> usize { + let mut l2s = self.l2_tables.borrow_mut(); + l2s.push(Box::new([Pte::default(); 1024])); + l2s.len() - 1 + } + + /// Map a single 4 KiB page at `va` with the given permissions, allocating tables/frames as needed. + fn map_page(&self, va: VirtualAddress, perms: Perms) { + let vpn1 = va.vpn1() as usize; + let vpn0 = va.vpn0() as usize; + let mut root = self.root.borrow_mut(); + if root[vpn1].next_l2.is_none() { + let l2_idx = self.allocate_l2(); + root[vpn1].next_l2 = Some(l2_idx); + root[vpn1].valid = true; + } + let l2_idx = root[vpn1].next_l2.expect("l2 table missing"); + drop(root); + + let mut l2s = self.l2_tables.borrow_mut(); + let l2 = &mut l2s[l2_idx]; + if !l2[vpn0].valid { + let frame = self + .allocate_frame() + .expect("out of physical frames while mapping"); + l2[vpn0].ppn = frame; + l2[vpn0].valid = true; + l2[vpn0].read = perms.read; + l2[vpn0].write = perms.write; + l2[vpn0].exec = perms.exec; + l2[vpn0].user = perms.user; + } + } + + /// Map a contiguous virtual range page-by-page with the given permissions. + pub fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { + let mut page_start = start.align_down(); + let mut remaining = len; + while remaining > 0 { + self.map_page(page_start, perms); + page_start = VirtualAddress(page_start.as_u32().wrapping_add(PAGE_SIZE as u32)); + if remaining > PAGE_SIZE { + remaining -= PAGE_SIZE; + } else { + remaining = 0; + } + } + } + + /// Translate a virtual address to a physical offset into `backing`, checking permissions. + fn translate(&self, va: VirtualAddress, kind: MemoryAccessKind) -> Option { + let vpn1 = va.vpn1() as usize; + let vpn0 = va.vpn0() as usize; + let offset = va.offset() as usize; + let root: Ref<'_, Box<[Pte; 1024]>> = self.root.borrow(); + let l2_idx = root.get(vpn1).and_then(|pte| pte.next_l2)?; + let l2s = self.l2_tables.borrow(); + let l2 = l2s.get(l2_idx)?; + let leaf = l2.get(vpn0)?; + if !leaf.valid || !leaf.is_leaf() { + return None; + } + // Basic permission check: align MemoryAccessKind to R/W. + let allowed = match kind { + MemoryAccessKind::Load | MemoryAccessKind::ReservationLoad => leaf.read || leaf.exec, + MemoryAccessKind::Store + | MemoryAccessKind::Atomic + | MemoryAccessKind::ReservationStore => leaf.write, + }; + if !allowed { + return None; + } + let pa = (leaf.ppn << PAGE_SHIFT) + offset; + Some(pa) + } + + fn meter_access( + metering: &mut dyn Metering, + kind: MemoryAccessKind, + addr: VirtualAddress, + bytes: usize, + ) -> bool { + matches!( + metering.on_memory_access(kind, addr.as_usize(), bytes), + MeterResult::Continue + ) + } + + /// Copy a slice into physical backing, honoring translation and page boundaries. + fn copy_into_backing(&self, start: VirtualAddress, data: &[u8], kind: MemoryAccessKind) { + let mut remaining = data.len(); + let mut offset_in_data = 0usize; + let mut va = start; + while remaining > 0 { + let phys = self + .translate(va, kind) + .expect("copy failed: unmapped virtual address"); + let page_remaining = PAGE_SIZE - (va.offset() as usize); + let to_copy = core::cmp::min(page_remaining, remaining); + { + let mut backing = self.backing.borrow_mut(); + let dst = phys; + let src_start = offset_in_data; + let src_end = src_start + to_copy; + backing[dst..dst + to_copy].copy_from_slice(&data[src_start..src_end]); + } + remaining -= to_copy; + offset_in_data += to_copy; + va = VirtualAddress(va.as_u32().wrapping_add(to_copy as u32)); + } + } +} + +impl Mmu for Memory { + fn mem(&self) -> Ref> { + self.backing.borrow() + } + + fn mem_slice( + &self, + start: VirtualAddress, + end: VirtualAddress, + ) -> Option> { + if start.as_usize() > end.as_usize() { + return None; + } + let len = end.as_usize().saturating_sub(start.as_usize()); + let phys_start = self.translate(start, MemoryAccessKind::Load)?; + let last_va = VirtualAddress(end.as_u32().saturating_sub(1)); + let phys_last = self.translate(last_va, MemoryAccessKind::Load)?; + // Ensure the range is physically contiguous. + if phys_last + 1 != phys_start + len { + return None; + } + let backing = self.backing.borrow(); + Some(std::cell::Ref::map(backing, move |v| { + &v[phys_start..phys_start + len] + })) + } + + fn store_u16( + &self, + addr: VirtualAddress, + val: u16, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 2) { + return false; + } + if let Some(offset) = self.translate(addr, kind) { + let mut backing = self.backing.borrow_mut(); + backing[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); + } else { + return false; + } + true + } + + fn store_u32( + &self, + addr: VirtualAddress, + val: u32, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 4) { + return false; + } + if let Some(offset) = self.translate(addr, kind) { + let mut backing = self.backing.borrow_mut(); + backing[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); + } else { + return false; + } + true + } + + fn store_u8( + &self, + addr: VirtualAddress, + val: u8, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> bool { + if !Self::meter_access(metering, kind, addr, 1) { + return false; + } + if let Some(offset) = self.translate(addr, kind) { + let mut backing = self.backing.borrow_mut(); + backing[offset] = val; + } else { + return false; + } + true + } + + fn load_u32( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(u32::from_le_bytes( + backing[offset..offset + 4].try_into().unwrap(), + )) + } + + fn load_byte( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 1) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(backing[offset]) + } + + fn load_halfword( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 2) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(u16::from_le_bytes( + backing[offset..offset + 2].try_into().unwrap(), + )) + } + + fn load_word( + &self, + addr: VirtualAddress, + metering: &mut dyn Metering, + kind: MemoryAccessKind, + ) -> Option { + if !Self::meter_access(metering, kind, addr, 4) { + return None; + } + let backing = self.backing.borrow(); + let offset = self.translate(addr, kind)?; + Some(u32::from_le_bytes( + backing[offset..offset + 4].try_into().unwrap(), + )) + } + + fn write_code(&self, start_addr: VirtualAddress, code: &[u8]) { + let end_addr = start_addr + .checked_add(code.len() as u32) + .expect("code write overflow"); + self.map_range(start_addr, code.len(), Perms::rwx_kernel()); + self.copy_into_backing(start_addr, code, MemoryAccessKind::Store); + self.next_heap + .set(VirtualAddress(end as u32 + HEAP_PTR_OFFSET)); + } + + fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { + let mut addr = self.next_heap.get().as_u32(); + let align = 8; + addr = (addr + (align - 1)) & !(align - 1); + let end = addr + data.len() as u32; + let start_va = VirtualAddress(addr); + self.map_range(start_va, data.len(), Perms::rw_kernel()); + + self.copy_into_backing(start_va, data, MemoryAccessKind::Store); + let end_va = VirtualAddress(end); + self.next_heap.set(end_va); + start_va + } + + fn stack_top(&self) -> VirtualAddress { + VirtualAddress(self.total_size() as u32) + } + + fn size(&self) -> usize { + self.total_size() + } + + fn offset(&self, addr: VirtualAddress) -> usize { + addr.as_usize() + } + + fn next_heap(&self) -> VirtualAddress { + self.next_heap.get() + } + + fn set_next_heap(&self, next: VirtualAddress) { + self.next_heap.set(next); + } +} diff --git a/crates/bootloader/src/memory/memory_page.rs b/crates/bootloader/src/memory/memory_page.rs deleted file mode 100644 index cba3ae0..0000000 --- a/crates/bootloader/src/memory/memory_page.rs +++ /dev/null @@ -1,332 +0,0 @@ -use std::cell::{Cell, Ref, RefCell}; -use std::convert::TryInto; -use std::rc::Rc; -use vm::memory::{HEAP_PTR_OFFSET, memory}; -use vm::metering::{MemoryAccessKind, MeterResult, Metering}; - -#[derive(Debug, Clone)] -pub struct MemoryPage { - mem: Rc>>, - pub next_heap: Cell, - pub base_address: usize, // base address for guest memory mapping -} - -impl MemoryPage { - pub fn new_with_base(memory_size: usize, base_address: usize) -> Self { - Self { - mem: Rc::new(RefCell::new(vec![0u8; memory_size])), - next_heap: Cell::new(0), - base_address, - } - } - - pub fn new(memory_size: usize) -> Self { - Self::new_with_base(memory_size, 0) - } - - pub fn mem(&self) -> Ref> { - self.mem.borrow() - } - - pub fn size(&self) -> usize { - let mem = self.mem(); - mem.len() - } - - pub fn offset(&self, addr: usize) -> usize { - addr.checked_sub(self.base_address) - .expect("Address below base_address") - } - - fn meter_access( - metering: &mut dyn Metering, - kind: MemoryAccessKind, - addr: usize, - bytes: usize, - ) -> bool { - matches!( - metering.on_memory_access(kind, addr, bytes), - MeterResult::Continue - ) - } - - pub fn store_u16( - &self, - addr: usize, - val: u16, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - if !Self::meter_access(metering, kind, addr, 2) { - return false; - } - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset + 2 > mem.len() { - panic!("store u16 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); - true - } - - pub fn store_u32( - &self, - addr: usize, - val: u32, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - if !Self::meter_access(metering, kind, addr, 4) { - return false; - } - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset + 4 > mem.len() { - panic!("store u32 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); - true - } - - pub fn store_u8( - &self, - addr: usize, - val: u8, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - if !Self::meter_access(metering, kind, addr, 1) { - return false; - } - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset >= mem.len() { - panic!("store u8 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset] = val; - true - } - - pub fn load_u32( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 4) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - if offset + 4 > mem.len() { - panic!("load u32 out of bounds: addr = 0x{:08x}", addr); - } - Some(u32::from_le_bytes( - mem[offset..offset + 4].try_into().unwrap(), - )) - } - - pub fn load_byte( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 1) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - Some(mem[offset]) - } - - pub fn load_halfword( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 2) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - Some(u16::from_le_bytes( - mem[offset..offset + 2].try_into().unwrap(), - )) - } - - pub fn load_word( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 4) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - Some(u32::from_le_bytes( - mem[offset..offset + 4].try_into().unwrap(), - )) - } - - pub fn mem_slice(&self, start: usize, end: usize) -> Option> { - let start_offset = self.offset(start); - let end_offset = self.offset(end); - let mem_ref = self.mem.borrow(); - if end_offset > mem_ref.len() || start_offset > end_offset { - return None; - } - Some(std::cell::Ref::map(mem_ref, move |v| { - &v[start_offset..end_offset] - })) - } - - pub fn write_code(&self, start_addr: usize, code: &[u8]) { - let start_offset = self.offset(start_addr); - let mut mem = self.mem.borrow_mut(); - let end = start_offset + code.len(); - mem[start_offset..end].copy_from_slice(code); - - // set heap pointer - self.next_heap - .set(start_offset as u32 + code.len() as u32 + HEAP_PTR_OFFSET); - } - - pub fn alloc_on_heap(&self, data: &[u8]) -> u32 { - let mut addr = self.next_heap.get(); - - // Align to 4 bytes (or 8 if you're storing u64s) - let align = 8; - addr = (addr + (align - 1)) & !(align - 1); - - let end = addr + data.len() as u32; - assert!( - end as usize <= self.size(), - "Out of memory: trying to allocate {} bytes, but only {} bytes available", - data.len(), - self.size() - addr as usize - ); - - self.mem.borrow_mut()[addr as usize..end as usize].copy_from_slice(data); - self.next_heap.set(end); - - addr - } - - pub fn stack_top(&self) -> u32 { - self.size() as u32 - } -} - -impl Default for MemoryPage { - fn default() -> Self { - MemoryPage::new(4096) - } -} - -impl memory for MemoryPage { - fn mem(&self) -> Ref> { - MemoryPage::mem(self) - } - - fn mem_slice(&self, start: usize, end: usize) -> Option> { - MemoryPage::mem_slice(self, start, end) - } - - fn store_u16( - &self, - addr: usize, - val: u16, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - MemoryPage::store_u16(self, addr, val, metering, kind) - } - - fn store_u32( - &self, - addr: usize, - val: u32, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - MemoryPage::store_u32(self, addr, val, metering, kind) - } - - fn store_u8( - &self, - addr: usize, - val: u8, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - MemoryPage::store_u8(self, addr, val, metering, kind) - } - - fn load_u32( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_u32(self, addr, metering, kind) - } - - fn load_byte( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_byte(self, addr, metering, kind) - } - - fn load_halfword( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_halfword(self, addr, metering, kind) - } - - fn load_word( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_word(self, addr, metering, kind) - } - - fn write_code(&self, start_addr: usize, code: &[u8]) { - MemoryPage::write_code(self, start_addr, code) - } - - fn alloc_on_heap(&self, data: &[u8]) -> u32 { - MemoryPage::alloc_on_heap(self, data) - } - - fn stack_top(&self) -> u32 { - MemoryPage::stack_top(self) - } - - fn size(&self) -> usize { - MemoryPage::size(self) - } - - fn offset(&self, addr: usize) -> usize { - MemoryPage::offset(self, addr) - } - - fn next_heap(&self) -> u32 { - self.next_heap.get() - } - - fn set_next_heap(&self, next: u32) { - self.next_heap.set(next); - } -} diff --git a/crates/bootloader/src/memory/mod.rs b/crates/bootloader/src/memory/mod.rs index 043d75e..a4989e8 100644 --- a/crates/bootloader/src/memory/mod.rs +++ b/crates/bootloader/src/memory/mod.rs @@ -1,7 +1,7 @@ //! Simple in-memory pages for the OS boot/runtime layers. -mod memory_page; -mod stacked_memory; +mod memory; +mod pte; -pub use memory_page::MemoryPage; -pub use stacked_memory::StackedMemory; +pub use memory::Memory; +pub(crate) use pte::Pte; diff --git a/crates/bootloader/src/memory/pte.rs b/crates/bootloader/src/memory/pte.rs new file mode 100644 index 0000000..edca6bd --- /dev/null +++ b/crates/bootloader/src/memory/pte.rs @@ -0,0 +1,33 @@ +/// Simple Sv32-style page table entry used by the software MMU. +/// +/// Layout mirrors the RISC-V Sv32 PTE fields: +/// - `V` (valid) gate keeps the entry. +/// - `R/W/X` control read/write/execute access. +/// - `U` marks user visibility (we keep `G/A/D` out for now). +/// - `PPN` holds the physical page number. +/// +/// For non-leaf entries, `next_l2` indexes an L2 table; for leaf entries, +/// `ppn` points at the mapped frame. +#[derive(Clone, Copy, Debug, Default)] +pub struct Pte { + /// Valid bit: entry is present. + pub valid: bool, + /// Read permission. + pub read: bool, + /// Write permission. + pub write: bool, + /// Execute permission. + pub exec: bool, + /// User visibility (false = supervisor/kernel only). + pub user: bool, + /// Physical page number for a leaf mapping. + pub ppn: usize, + /// Index of next-level L2 table for non-leaf entries. + pub next_l2: Option, +} + +impl Pte { + pub fn is_leaf(&self) -> bool { + self.valid && (self.read || self.write || self.exec) + } +} diff --git a/crates/bootloader/src/memory/stacked_memory.rs b/crates/bootloader/src/memory/stacked_memory.rs deleted file mode 100644 index a5106c7..0000000 --- a/crates/bootloader/src/memory/stacked_memory.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::rc::Rc; - -use super::MemoryPage; -use vm::memory::Memory; - -/// Manages a stack of memory pages allocated in order. -#[derive(Debug)] -pub struct StackedMemory { - pub page_size: usize, - max_pages: usize, - pages: Vec, -} - -impl StackedMemory { - pub fn new(max_pages: usize, page_size: usize) -> Self { - assert!(max_pages != 0, "max_pages must be > 0"); - assert!(page_size != 0, "page_size must be > 0"); - - Self { - page_size, - max_pages, - pages: Vec::with_capacity(max_pages), - } - } - - /// Creates and owns a new page. - pub fn new_page(&mut self) -> Memory { - if self.pages.len() >= self.max_pages { - panic!( - "out of memory: maximum page count ({}) reached", - self.max_pages - ); - } - - let page: Memory = Rc::new(MemoryPage::new(self.page_size)); - self.pages.push(Rc::clone(&page)); - page - } - - pub fn pop_page(&mut self) { - self.pages.pop(); - } - - pub fn get_page(&self, index: usize) -> Option { - self.pages.get(index).cloned() - } - - pub fn top_page(&self) -> Option { - self.pages.last().cloned() - } - - pub fn count(&self) -> usize { - self.pages.len() - } -} diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index c4d2450..a06829f 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use state::State; use types::{ADDRESS_LEN, address::Address, result::RESULT_SIZE}; use vm::host_interface::HostInterface; -use vm::memory::{HEAP_PTR_OFFSET, Memory}; +use vm::memory::{HEAP_PTR_OFFSET, Memory, VirtualAddress}; use vm::metering::{MeterResult, Metering}; use vm::registers::Register; use vm::sys_call::{ @@ -125,7 +125,8 @@ impl DefaultSyscallHandler { // EDUCATIONAL: Safely read the key from memory // EDUCATIONAL: Create a limited scope to avoid borrow checker issues - let event_bytes = match borrowed_memory.mem_slice(ptr, ptr + len) { + let (start, end) = va_range(ptr, len); + let event_bytes = match borrowed_memory.mem_slice(start, end) { Some(r) => r, None => panic!("invalid memory access"), // Invalid memory access }; @@ -160,17 +161,17 @@ impl DefaultSyscallHandler { let borrowed_memory = memory.as_ref(); // Parse address - let address_slice_ref = - match borrowed_memory.mem_slice(address_ptr, address_ptr + ADDRESS_LEN) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid address memory access: ptr={}, len={}", - address_ptr, ADDRESS_LEN - ); - return 0; - } - }; + let (address_start, address_end) = va_range(address_ptr, ADDRESS_LEN); + let address_slice_ref = match borrowed_memory.mem_slice(address_start, address_end) { + Some(r) => r, + None => { + println!( + "❌ Storage GET - Invalid address memory access: ptr={}, len={}", + address_ptr, ADDRESS_LEN + ); + return 0; + } + }; let address_bytes = address_slice_ref.as_ref(); let address_hex = address_bytes .iter() @@ -183,17 +184,17 @@ impl DefaultSyscallHandler { // Parse domain let domain_slice = { - let domain_slice_ref = - match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid domain memory access: ptr={}, len={}", - domain_ptr, domain_len - ); - return 0; - } - }; + let (domain_start, domain_end) = va_range(domain_ptr, domain_len); + let domain_slice_ref = match borrowed_memory.mem_slice(domain_start, domain_end) { + Some(r) => r, + None => { + println!( + "❌ Storage GET - Invalid domain memory access: ptr={}, len={}", + domain_ptr, domain_len + ); + return 0; + } + }; domain_slice_ref.as_ref().to_vec() }; let domain = match core::str::from_utf8(&domain_slice) { @@ -209,7 +210,8 @@ impl DefaultSyscallHandler { // Parse key let key_slice = { - let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { + let (key_start, key_end) = va_range(key_ptr, key_len); + let key_slice_ref = match borrowed_memory.mem_slice(key_start, key_end) { Some(r) => r, None => { println!( @@ -258,7 +260,7 @@ impl DefaultSyscallHandler { "✅ Found value for address: '{}', domain: '{}', Key: '{}'", address_hex, domain, display_key ); - return addr; + return addr.as_u32(); } else { println!( "❌ No value found for address: '{}', domain: '{}', key: '{}'", @@ -298,17 +300,17 @@ impl DefaultSyscallHandler { let borrowed_memory = memory.as_ref(); // Parse address - let address_slice_ref = - match borrowed_memory.mem_slice(address_ptr, address_ptr + ADDRESS_LEN) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid address memory access: ptr={}, len={}", - address_ptr, ADDRESS_LEN - ); - return 0; - } - }; + let (address_start, address_end) = va_range(address_ptr, ADDRESS_LEN); + let address_slice_ref = match borrowed_memory.mem_slice(address_start, address_end) { + Some(r) => r, + None => { + println!( + "❌ Storage SET - Invalid address memory access: ptr={}, len={}", + address_ptr, ADDRESS_LEN + ); + return 0; + } + }; let address_bytes = address_slice_ref.as_ref(); let address_hex = address_bytes .iter() @@ -320,8 +322,8 @@ impl DefaultSyscallHandler { let address = Address(addr_arr); // Parse domain - let domain_slice_ref = match borrowed_memory.mem_slice(domain_ptr, domain_ptr + domain_len) - { + let (domain_start, domain_end) = va_range(domain_ptr, domain_len); + let domain_slice_ref = match borrowed_memory.mem_slice(domain_start, domain_end) { Some(r) => r, None => { println!( @@ -344,7 +346,8 @@ impl DefaultSyscallHandler { }; // Parse key - let key_slice_ref = match borrowed_memory.mem_slice(key_ptr, key_ptr + key_len) { + let (key_start, key_end) = va_range(key_ptr, key_len); + let key_slice_ref = match borrowed_memory.mem_slice(key_start, key_end) { Some(r) => r, None => { println!( @@ -375,7 +378,8 @@ impl DefaultSyscallHandler { }; // Parse value - let value_slice_ref = match borrowed_memory.mem_slice(val_ptr, val_ptr + val_len) { + let (val_start, val_end) = va_range(val_ptr, val_len); + let value_slice_ref = match borrowed_memory.mem_slice(val_start, val_end) { Some(r) => r, None => { println!( @@ -407,8 +411,9 @@ impl DefaultSyscallHandler { fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Memory) -> u32 { let msg_ptr = regs[Register::A0 as usize] as usize; let msg_len = regs[Register::A1 as usize] as usize; + let (msg_start, msg_end) = va_range(msg_ptr, msg_len); let msg = memory - .mem_slice(msg_ptr, msg_ptr + msg_len) + .mem_slice(msg_start, msg_end) .map(|bytes| String::from_utf8_lossy(bytes.as_ref()).into_owned()) .unwrap_or_else(|| "".to_string()); panic!("🔥 Guest panic: {}", msg); @@ -424,14 +429,14 @@ impl DefaultSyscallHandler { panic!("Metering halted SYSCALL_LOG"); } let borrowed_memory = memory.as_ref(); - let fmt_slice = - match borrowed_memory.mem_slice(fmt_ptr as usize, (fmt_ptr + fmt_len) as usize) { - Some(s) => s, - None => { - println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); - return 0; - } - }; + let (fmt_start, fmt_end) = va_range(fmt_ptr as usize, fmt_len as usize); + let fmt_slice = match borrowed_memory.mem_slice(fmt_start, fmt_end) { + Some(s) => s, + None => { + println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); + return 0; + } + }; let fmt_bytes = fmt_slice.as_ref(); let fmt = match core::str::from_utf8(fmt_bytes) { Ok(s) => s, @@ -442,8 +447,8 @@ impl DefaultSyscallHandler { return 0; } }; - let args_bytes_slice = - borrowed_memory.mem_slice(arg_ptr as usize, (arg_ptr + arg_len) as usize); + let (args_start, args_end) = va_range(arg_ptr as usize, arg_len as usize); + let args_bytes_slice = borrowed_memory.mem_slice(args_start, args_end); let args_bytes_holder; let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { args_bytes_holder = slice; @@ -471,7 +476,8 @@ impl DefaultSyscallHandler { 's' => { let ptr = next() as usize; let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { Some(slice) => { let s_ptr = core::str::from_utf8(slice.as_ref()); args.push(match s_ptr { @@ -487,7 +493,8 @@ impl DefaultSyscallHandler { 'b' => { let ptr = next() as usize; let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { Some(slice) => { args.push(Arg::Bytes(slice.to_vec())); } @@ -501,7 +508,8 @@ impl DefaultSyscallHandler { let ptr = next() as usize; let len = next() as usize; let byte_len = len * 4; // u32 is 4 bytes - match borrowed_memory.mem_slice(ptr, ptr + byte_len) { + let (start, end) = va_range(ptr, byte_len); + match borrowed_memory.mem_slice(start, end) { Some(slice) => { args.push(Arg::Bytes(slice.to_vec())); } @@ -514,7 +522,8 @@ impl DefaultSyscallHandler { // Array of u8s let ptr = next() as usize; let len = next() as usize; - match borrowed_memory.mem_slice(ptr, ptr + len) { + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { Some(slice) => { args.push(Arg::Bytes(slice.to_vec())); } @@ -632,15 +641,18 @@ impl DefaultSyscallHandler { } { let borrowed_memory = memory.as_ref(); - let to_slice = match borrowed_memory.mem_slice(to_ptr, to_ptr + 20) { + let (to_start, to_end) = va_range(to_ptr, 20); + let to_slice = match borrowed_memory.mem_slice(to_start, to_end) { Some(r) => r, None => return 0, }; - let from_slice = match borrowed_memory.mem_slice(from_ptr, from_ptr + 20) { + let (from_start, from_end) = va_range(from_ptr, 20); + let from_slice = match borrowed_memory.mem_slice(from_start, from_end) { Some(r) => r, None => return 0, }; - let input_slice = match borrowed_memory.mem_slice(input_ptr, input_ptr + input_len) { + let (input_start, input_end) = va_range(input_ptr, input_len); + let input_slice = match borrowed_memory.mem_slice(input_start, input_end) { Some(r) => r, None => return 0, }; @@ -660,7 +672,7 @@ impl DefaultSyscallHandler { if matches!(metering.on_alloc(result_bytes.len()), MeterResult::Halt) { panic!("Metering halted alloc for call_program result"); } - borrowed_memory.alloc_on_heap(&result_bytes) + borrowed_memory.alloc_on_heap(&result_bytes).as_u32() } } @@ -686,26 +698,26 @@ impl DefaultSyscallHandler { let current_heap = memory.next_heap(); // Initialize heap pointer if not set (no code has been written) - if current_heap == 0 { - memory.set_next_heap(HEAP_PTR_OFFSET); + if current_heap.as_u32() == 0 { + memory.set_next_heap(VirtualAddress(HEAP_PTR_OFFSET)); } // Allocate aligned memory on heap let data = vec![0u8; size]; let ptr = memory.alloc_on_heap(&data); - if ptr == 0 { + if ptr.as_u32() == 0 { println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); return 0; } // Check if allocated address meets alignment requirements - if (ptr as usize) % align != 0 { + if ptr.as_usize() % align != 0 { // Re-allocate with enough space for alignment let total_size = size + align - 1; let padded_data = vec![0u8; total_size]; let padded_ptr = memory.alloc_on_heap(&padded_data); - if padded_ptr == 0 { + if padded_ptr.as_u32() == 0 { println!( "VM Alloc: Out of memory, failed to allocate {} bytes for alignment", total_size @@ -713,11 +725,11 @@ impl DefaultSyscallHandler { return 0; } // Return properly aligned pointer within the allocated region - let aligned_ptr = ((padded_ptr as usize + align - 1) & !(align - 1)) as u32; + let aligned_ptr = ((padded_ptr.as_usize() + align - 1) & !(align - 1)) as u32; return aligned_ptr; } - ptr + ptr.as_u32() } fn sys_dealloc(&mut self, args: [u32; 6], _memory: Memory, metering: &mut dyn Metering) -> u32 { @@ -752,9 +764,8 @@ impl DefaultSyscallHandler { } let borrowed = memory.as_ref(); - let to_slice = borrowed - .mem_slice(to_ptr, to_ptr + 20) - .expect("invalid to ptr"); + let (to_start, to_end) = va_range(to_ptr, 20); + let to_slice = borrowed.mem_slice(to_start, to_end).expect("invalid to ptr"); let mut to = [0u8; 20]; to.copy_from_slice(to_slice.as_ref()); @@ -779,8 +790,9 @@ impl DefaultSyscallHandler { } let addr = { let borrowed = memory.as_ref(); + let (addr_start, addr_end) = va_range(addr_ptr, 20); let addr_slice = borrowed - .mem_slice(addr_ptr, addr_ptr + 20) + .mem_slice(addr_start, addr_end) .expect("invalid addr ptr"); let mut addr = [0u8; 20]; addr.copy_from_slice(addr_slice.as_ref()); @@ -788,7 +800,7 @@ impl DefaultSyscallHandler { }; let bal = host.balance(addr); - memory.alloc_on_heap(&bal.to_le_bytes()) + memory.alloc_on_heap(&bal.to_le_bytes()).as_u32() } fn sys_commit_state( @@ -830,7 +842,8 @@ impl DefaultSyscallHandler { } let borrowed = memory.as_ref(); - let address_slice = match borrowed.mem_slice(addr_ptr, addr_ptr + ADDRESS_LEN) { + let (addr_start, addr_end) = va_range(addr_ptr, ADDRESS_LEN); + let address_slice = match borrowed.mem_slice(addr_start, addr_end) { Some(r) => r, None => return 1, }; @@ -838,7 +851,8 @@ impl DefaultSyscallHandler { addr_bytes.copy_from_slice(address_slice.as_ref()); let address = Address(addr_bytes); - let code_slice = match borrowed.mem_slice(code_ptr, code_ptr + code_len) { + let (code_start, code_end) = va_range(code_ptr, code_len); + let code_slice = match borrowed.mem_slice(code_start, code_end) { Some(r) => r, None => return 1, }; @@ -851,3 +865,9 @@ impl DefaultSyscallHandler { 0 } } + +fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { + let start = VirtualAddress(ptr as u32); + let end = start.wrapping_add(len as u32); + (start, end) +} diff --git a/crates/bootloader/tests/allocator_test.rs b/crates/bootloader/tests/allocator_test.rs index 75b1e7c..dcd50f5 100644 --- a/crates/bootloader/tests/allocator_test.rs +++ b/crates/bootloader/tests/allocator_test.rs @@ -1,16 +1,16 @@ -use bootloader::memory::MemoryPage; +use bootloader::memory::Memory as BootMemory; use bootloader::DefaultSyscallHandler; use state::State; use std::cell::RefCell; use std::rc::Rc; use vm::host_interface; -use vm::memory::Memory; +use vm::memory::{Memory, PAGE_SIZE}; use vm::metering::NoopMeter; use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); @@ -46,7 +46,7 @@ fn test_allocator_syscalls() { #[test] fn test_multiple_allocations() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); @@ -88,7 +88,7 @@ fn test_multiple_allocations() { #[test] fn test_alignment_requirements() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); @@ -116,7 +116,7 @@ fn test_alignment_requirements() { #[test] fn test_invalid_alignment() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); + let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); diff --git a/crates/bootloader/tests/memory_page_offset.rs b/crates/bootloader/tests/memory_page_offset.rs deleted file mode 100644 index 74fdcaa..0000000 --- a/crates/bootloader/tests/memory_page_offset.rs +++ /dev/null @@ -1,84 +0,0 @@ -use bootloader::memory::MemoryPage; -use vm::metering::{MemoryAccessKind, NoopMeter}; - -#[test] -fn test_offset_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - let mut meter = NoopMeter::default(); - assert_eq!(mem.offset(0), 0); - assert_eq!(mem.offset(100), 100); - assert_eq!(mem.offset(1023), 1023); - assert_eq!(mem.load_u32(0, &mut meter, MemoryAccessKind::Load), Some(0)); -} - -#[test] -fn test_offset_high_base() { - let base = 0x8000_0000; - let mem = MemoryPage::new_with_base(1024, base); - assert_eq!(mem.offset(base), 0); - assert_eq!(mem.offset(base + 100), 100); - assert_eq!(mem.offset(base + 1023), 1023); -} - -#[test] -#[should_panic(expected = "Address below base_address")] -fn test_offset_below_base_panics() { - let base = 0x8000_0000; - let mem = MemoryPage::new_with_base(1024, base); - mem.offset(base - 1); -} - -#[test] -fn test_store_and_load_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - let mut meter = NoopMeter::default(); - assert!(mem.store_u8(10, 0xAB, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_byte(10, &mut meter, MemoryAccessKind::Load), - Some(0xAB) - ); - assert!(mem.store_u16(20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_halfword(20, &mut meter, MemoryAccessKind::Load), - Some(0xCDEF) - ); - assert!(mem.store_u32(30, 0x1234_5678, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_u32(30, &mut meter, MemoryAccessKind::Load), - Some(0x1234_5678) - ); -} - -#[test] -fn test_store_and_load_high_base() { - let base = 0x8000_0000; - let mem = MemoryPage::new_with_base(1024, base); - let mut meter = NoopMeter::default(); - assert!(mem.store_u8(base + 10, 0xAB, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_byte(base + 10, &mut meter, MemoryAccessKind::Load), - Some(0xAB) - ); - assert!(mem.store_u16(base + 20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_halfword(base + 20, &mut meter, MemoryAccessKind::Load), - Some(0xCDEF) - ); - assert!(mem.store_u32(base + 30, 0x1234_5678, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_u32(base + 30, &mut meter, MemoryAccessKind::Load), - Some(0x1234_5678) - ); -} - -#[test] -fn test_store_and_load_at_offset_zero() { - let base = 0x8000_0000; - let mem = MemoryPage::new_with_base(1024, base); - let mut meter = NoopMeter::default(); - assert!(mem.store_u8(base, 0xAA, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_byte(base, &mut meter, MemoryAccessKind::Load), - Some(0xAA) - ); -} diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index d5a06a9..c5c5ca1 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -49,7 +49,6 @@ pub struct TestRunner { writer: Rc>, verbose: bool, vm_memory_size: usize, - max_memory_pages: usize, kernel_bytes: Option>, kernel_path: Option, } @@ -66,12 +65,6 @@ impl TestRunner { self } - /// Set max memory pages - pub fn with_max_pages(mut self, pages: usize) -> Self { - self.max_memory_pages = pages; - self - } - /// Enable or disable verbose mode pub fn with_verbose(mut self, verbose: bool) -> Self { self.verbose = verbose; @@ -90,7 +83,6 @@ impl TestRunner { writer, verbose: false, vm_memory_size: 512 * 1024, // larger default to accommodate bigger binaries without RVC - max_memory_pages: 128, // allow more pages for larger programs kernel_bytes: Self::load_kernel_from_env(), kernel_path: env::var("KERNEL_ELF").ok(), } @@ -126,7 +118,7 @@ impl TestRunner { /// Run a single test case fn run_test_case(&self, case: &super::TestCase) -> Result<(), String> { - let mut bootloader = Bootloader::new(self.max_memory_pages, self.vm_memory_size); + let mut bootloader = Bootloader::new(self.vm_memory_size); let state = Rc::new(RefCell::new(State::new())); // Write test case header diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index d42d500..648b3e8 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -1,7 +1,7 @@ use crate::decoder::{decode_compressed, decode_full}; use crate::host_interface::HostInterface; use crate::instruction::Instruction; -use crate::memory::Memory; +use crate::memory::{Memory, VirtualAddress}; use crate::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; use crate::sys_call::SyscallHandler; use core::cell::RefCell; @@ -66,7 +66,7 @@ pub struct CPU { /// Reservation address for LR/SC atomic operations /// EDUCATIONAL: This implements the Load-Reserved/Store-Conditional /// mechanism for atomic memory operations in RISC-V - pub reservation_addr: Option, + pub reservation_addr: Option, /// Optional writer for verbose output /// If None, uses println! to console @@ -248,7 +248,9 @@ impl CPU { ) -> bool { // EDUCATIONAL: Debug output to help understand what's happening // Get the actual instruction bytes for debugging - if let Some(bytes) = memory.mem_slice(self.pc as usize, self.pc as usize + size as usize) { + let pc_va = VirtualAddress(self.pc); + let end_va = VirtualAddress(self.pc.wrapping_add(size as u32)); + if let Some(bytes) = memory.mem_slice(pc_va, end_va) { let hex_bytes = bytes .iter() .map(|b| format!("{:02x}", b)) @@ -302,7 +304,8 @@ impl CPU { /// RETURN VALUE: Returns false to halt execution on invalid instructions fn unknown_instruction(&mut self, memory: Memory) -> bool { // EDUCATIONAL: Try to read the invalid instruction bytes for debugging - if let Some(slice_ref) = memory.mem_slice(self.pc as usize, self.pc as usize + 4) { + if let Some(slice_ref) = memory.mem_slice(VirtualAddress(self.pc), VirtualAddress(self.pc.wrapping_add(4))) + { // EDUCATIONAL: Convert bytes to hex for human-readable debugging let hex_dump = slice_ref .iter() @@ -335,10 +338,10 @@ impl CPU { /// /// RETURN VALUE: Returns Some((instruction, size)) if successful, None if invalid pub fn next_instruction(&mut self, memory: Memory) -> Option<(Instruction, u8)> { - let pc = self.pc as usize; + let pc = VirtualAddress(self.pc); // EDUCATIONAL: Read 4 bytes from memory (enough for any instruction) - let bytes = memory.mem_slice(pc, pc + 4)?; + let bytes = memory.mem_slice(pc, VirtualAddress(self.pc.wrapping_add(4)))?; // EDUCATIONAL: Need at least 2 bytes for any instruction if bytes.len() < 2 { diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index 4c70ebd..d9b63a3 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,4 +1,5 @@ use super::{Instruction, MemoryAccessKind, Memory, CPU}; +use crate::memory::VirtualAddress; use crate::host_interface::HostInterface; use crate::instruction::CsrOp; use crate::registers::Register; @@ -266,7 +267,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let val = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { Some(v) => v, @@ -283,7 +284,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let val = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Load) { Some(v) => v, @@ -299,7 +300,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let byte = match memory.load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { Some(v) => v, @@ -316,7 +317,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let byte = match memory.load_byte(addr, self.metering.as_mut(), MemoryAccessKind::Load) { Some(v) => v, @@ -332,7 +333,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let halfword = match memory.load_halfword( addr, self.metering.as_mut(), @@ -352,7 +353,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let halfword = match memory.load_halfword( addr, self.metering.as_mut(), @@ -373,7 +374,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let src = match self.read_reg(rs2) { Some(v) => v, None => return false, @@ -393,7 +394,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let src = match self.read_reg(rs2) { Some(v) => v, None => return false, @@ -408,7 +409,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base.wrapping_add(offset as u32) as usize; + let addr = VirtualAddress(base.wrapping_add(offset as u32)); let src = match self.read_reg(rs2) { Some(v) => v, None => return false, @@ -1028,7 +1029,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1050,7 +1051,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1078,7 +1079,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1106,7 +1107,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1134,7 +1135,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1162,7 +1163,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1194,7 +1195,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1226,7 +1227,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1254,7 +1255,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let orig = match memory.load_u32(addr, self.metering.as_mut(), MemoryAccessKind::Atomic) { Some(v) => v, @@ -1279,7 +1280,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let value = match memory.load_u32( addr, self.metering.as_mut(), @@ -1299,7 +1300,7 @@ impl CPU { Some(v) => v, None => return false, }; - let addr = base as usize; + let addr = VirtualAddress(base); let value_to_store = match self.read_reg(rs2) { Some(v) => v, None => return false, diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory.rs index cd8f3d9..6a5eb07 100644 --- a/crates/vm/src/memory.rs +++ b/crates/vm/src/memory.rs @@ -4,23 +4,84 @@ use crate::metering::{Metering, MemoryAccessKind}; pub const HEAP_PTR_OFFSET: u32 = 0x100; -pub trait memory: std::fmt::Debug { +pub const PAGE_SIZE: usize = 4096; +pub const PAGE_SHIFT: u32 = 12; +pub const VPN_MASK: u32 = 0x3ff; +pub const PAGE_OFFSET_MASK: u32 = 0xfff; + +/// Sv32 virtual address helper newtype. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VirtualAddress(pub u32); + +impl VirtualAddress { + pub fn as_u32(self) -> u32 { + self.0 + } + + pub fn as_usize(self) -> usize { + self.0 as usize + } + + pub fn offset(self) -> u32 { + self.0 & PAGE_OFFSET_MASK + } + + pub fn vpn0(self) -> u32 { + (self.0 >> PAGE_SHIFT) & VPN_MASK + } + + pub fn vpn1(self) -> u32 { + (self.0 >> (PAGE_SHIFT + 10)) & VPN_MASK + } + + pub fn align_down(self) -> Self { + VirtualAddress(self.0 & !(PAGE_OFFSET_MASK)) + } + + pub fn wrapping_add(self, value: u32) -> Self { + VirtualAddress(self.0.wrapping_add(value)) + } + + pub fn checked_add(self, value: u32) -> Option { + self.0.checked_add(value).map(VirtualAddress) + } +} + +impl From for VirtualAddress { + fn from(value: u32) -> Self { + VirtualAddress(value) + } +} + +impl From for VirtualAddress { + fn from(value: usize) -> Self { + VirtualAddress(value as u32) + } +} + +impl From for usize { + fn from(value: VirtualAddress) -> Self { + value.as_usize() + } +} + +pub trait Mmu: std::fmt::Debug { fn mem(&self) -> Ref>; - fn mem_slice(&self, start: usize, end: usize) -> Option>; - fn store_u16(&self, addr: usize, val: u16, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; - fn store_u32(&self, addr: usize, val: u32, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; - fn store_u8(&self, addr: usize, val: u8, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; - fn load_u32(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; - fn load_byte(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; - fn load_halfword(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; - fn load_word(&self, addr: usize, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; - fn write_code(&self, start_addr: usize, code: &[u8]); - fn alloc_on_heap(&self, data: &[u8]) -> u32; - fn stack_top(&self) -> u32; + fn mem_slice(&self, start: VirtualAddress, end: VirtualAddress) -> Option>; + fn store_u16(&self, addr: VirtualAddress, val: u16, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn store_u32(&self, addr: VirtualAddress, val: u32, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn store_u8(&self, addr: VirtualAddress, val: u8, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; + fn load_u32(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_byte(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_halfword(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn load_word(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + fn write_code(&self, start_addr: VirtualAddress, code: &[u8]); + fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress; + fn stack_top(&self) -> VirtualAddress; fn size(&self) -> usize; - fn offset(&self, addr: usize) -> usize; - fn next_heap(&self) -> u32; - fn set_next_heap(&self, next: u32); + fn offset(&self, addr: VirtualAddress) -> usize; + fn next_heap(&self) -> VirtualAddress; + fn set_next_heap(&self, next: VirtualAddress); } -pub type Memory = Rc; +pub type Memory = Rc; diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 0c51bfd..a339b6d 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,6 +1,6 @@ use crate::cpu::CPU; use crate::host_interface::HostInterface; -use crate::memory::Memory; +use crate::memory::{Memory, VirtualAddress}; use crate::metering::Metering; use crate::registers::Register; use crate::sys_call::SyscallHandler; @@ -49,7 +49,7 @@ impl VM { syscall_handler: Box, ) -> Self { let mut cpu = CPU::new(syscall_handler); - cpu.regs[Register::Sp as usize] = memory.stack_top(); + cpu.regs[Register::Sp as usize] = memory.stack_top().as_u32(); Self { cpu, memory, @@ -77,7 +77,7 @@ impl VM { /// to ensure proper alignment and to avoid conflicts with system memory. pub fn set_code(&mut self, alloc_add: u32, start_addr: u32, code: &[u8]) { // EDUCATIONAL: Write the program code to memory starting at address 0 - self.memory.write_code(alloc_add as usize, code); + self.memory.write_code(VirtualAddress(alloc_add), code); // EDUCATIONAL: Set the program counter to the starting address self.cpu.pc = start_addr; @@ -94,7 +94,7 @@ impl VM { /// /// RETURN VALUE: Returns the address where the data was written pub fn alloc_and_write(&mut self, data: &[u8]) -> u32 { - self.memory.alloc_on_heap(data) + self.memory.alloc_on_heap(data).as_u32() } /// Sets a register to point to data in memory. @@ -174,7 +174,7 @@ impl VM { // EDUCATIONAL: Show heap pointer for context let next_heap = borrowed_memory.next_heap(); println!("--- Memory Dump ---"); - println!("Next heap pointer: 0x{:08x}", next_heap); + println!("Next heap pointer: 0x{:08x}", next_heap.as_u32()); // EDUCATIONAL: Display memory in 16-byte lines for addr in (start..end).step_by(16) { From e9103ce604c76251c3420df35a87ce1f019ae0d9 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 12:59:42 +0200 Subject: [PATCH 16/70] Add Linux-style memory syscall IDs and minimal mmap handling --- crates/bootloader/src/memory/mod.rs | 2 +- crates/bootloader/src/syscalls.rs | 94 +++++++++++++++++++++++++++-- crates/vm/src/sys_call.rs | 5 ++ 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/crates/bootloader/src/memory/mod.rs b/crates/bootloader/src/memory/mod.rs index a4989e8..81428a1 100644 --- a/crates/bootloader/src/memory/mod.rs +++ b/crates/bootloader/src/memory/mod.rs @@ -3,5 +3,5 @@ mod memory; mod pte; -pub use memory::Memory; +pub use memory::{Memory, Perms}; pub(crate) use pte::Pte; diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index a06829f..716ecfc 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -3,16 +3,18 @@ use core::fmt::Write; use std::any::Any; use std::rc::Rc; +use crate::memory::Perms; use state::State; use types::{ADDRESS_LEN, address::Address, result::RESULT_SIZE}; use vm::host_interface::HostInterface; -use vm::memory::{HEAP_PTR_OFFSET, Memory, VirtualAddress}; +use vm::memory::{HEAP_PTR_OFFSET, Memory, VirtualAddress, PAGE_SIZE}; use vm::metering::{MeterResult, Metering}; use vm::registers::Register; use vm::sys_call::{ - SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_CREATE_ACCOUNT, - SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, - SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, SYSCALL_COMMIT_STATE, SyscallHandler, + SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_COMMIT_STATE, + SYSCALL_CREATE_ACCOUNT, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_MMAP, + SYSCALL_MPROTECT, SYSCALL_MUNMAP, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, + SYSCALL_TRANSFER, SyscallHandler, }; /// Represents different types of arguments that can be passed to system calls. @@ -91,6 +93,10 @@ impl SyscallHandler for DefaultSyscallHandler { SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), SYSCALL_COMMIT_STATE => self.sys_commit_state(args, memory, metering), SYSCALL_CREATE_ACCOUNT => self.sys_create_account(args, memory, metering), + SYSCALL_MMAP => self.sys_mmap(args, memory, metering), + SYSCALL_MUNMAP => self.sys_munmap(args, memory, metering), + SYSCALL_MPROTECT => self.sys_mprotect(args, memory, metering), + SYSCALL_BRK => self.sys_brk(args, memory, metering), _ => { panic!("Unknown syscall: {}", call_id); } @@ -864,6 +870,73 @@ impl DefaultSyscallHandler { account.is_contract = !account.code.is_empty(); 0 } + + // ===== Linux-like memory syscalls (stubs) ===== + fn sys_mmap(&mut self, args: [u32; 6], memory: Memory, _metering: &mut dyn Metering) -> u32 { + // a0=addr (hint), a1=len, a2=prot, a3=flags, a4=fd, a5=offset + let addr_hint = args[0]; + let len = args[1] as usize; + let prot = args[2]; + if len == 0 { + return 0; + } + // Align length to page size + let aligned_len = (len + (PAGE_SIZE - 1)) & !(PAGE_SIZE - 1); + let perms = prot_to_perms(prot); + + // Choose base VA: use hint if provided, else bump next_heap. + let base = if addr_hint != 0 { + VirtualAddress(addr_hint) + } else { + let hint = memory.next_heap(); + VirtualAddress((hint.as_u32() + (PAGE_SIZE as u32 - 1)) & !(PAGE_SIZE as u32 - 1)) + }; + + memory.map_range(base, aligned_len, perms); + if addr_hint == 0 { + let new_break = base + .checked_add(aligned_len as u32) + .unwrap_or(VirtualAddress(0)); + memory.set_next_heap(new_break); + } + base.as_u32() + } + + fn sys_munmap( + &mut self, + _args: [u32; 6], + _memory: Memory, + _metering: &mut dyn Metering, + ) -> u32 { + // Not implemented yet; return success. + 0 + } + + fn sys_mprotect( + &mut self, + _args: [u32; 6], + _memory: Memory, + _metering: &mut dyn Metering, + ) -> u32 { + // Not implemented yet; return success. + 0 + } + + fn sys_brk(&mut self, _args: [u32; 6], _memory: Memory, _metering: &mut dyn Metering) -> u32 { + // a0 = new_break; if 0, return current break + let new_brk = _args[0]; + let current = _memory.next_heap().as_u32(); + if new_brk == 0 { + return current; + } + if new_brk >= current { + _memory.set_next_heap(VirtualAddress(new_brk)); + new_brk + } else { + // Do not shrink in this minimal implementation. + current + } + } } fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { @@ -871,3 +944,16 @@ fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { let end = start.wrapping_add(len as u32); (start, end) } + +fn prot_to_perms(prot: u32) -> Perms { + // Map PROT_* bits (POSIX) to internal permission flags. + let read = prot & 0x1 != 0; + let write = prot & 0x2 != 0; + let exec = prot & 0x4 != 0; + Perms { + read, + write, + exec, + user: true, + } +} diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index f335718..8acc01f 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -16,6 +16,11 @@ pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; pub const SYSCALL_COMMIT_STATE: u32 = 11; pub const SYSCALL_CREATE_ACCOUNT: u32 = 12; +// Linux/RISC-V memory management syscall numbers: +pub const SYSCALL_MMAP: u32 = 222; // mmap(2): map pages with PROT/FLAGS +pub const SYSCALL_MUNMAP: u32 = 215; // munmap(2): unmap a VA range +pub const SYSCALL_MPROTECT: u32 = 226; // mprotect(2): change page protections +pub const SYSCALL_BRK: u32 = 214; // brk(2): set program break (heap end) /// Trait implemented by syscall handlers consumed by the VM. pub trait SyscallHandler: std::fmt::Debug { From df454c3c474157fa6afcee02b0990bddec609067 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 14:09:22 +0200 Subject: [PATCH 17/70] Refine MMU API and document memory syscalls --- crates/bootloader/src/bootloader.rs | 32 +++++++- crates/bootloader/src/memory/memory.rs | 104 ++++++++++++------------- crates/bootloader/src/memory/mod.rs | 3 +- crates/bootloader/src/syscalls.rs | 11 ++- crates/kernel/src/lib.rs | 3 + crates/kernel/src/main.rs | 35 +++++++-- crates/kernel/src/task.rs | 64 +++++++++++++++ crates/types/src/boot.rs | 29 +++++++ crates/types/src/lib.rs | 3 + crates/vm/src/memory.rs | 34 +++++++- 10 files changed, 250 insertions(+), 68 deletions(-) create mode 100644 crates/kernel/src/task.rs create mode 100644 crates/types/src/boot.rs diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 073e02a..e4f4dff 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -1,16 +1,17 @@ +use core::{mem, slice}; use std::cell::RefCell; use std::rc::Rc; use std::vec::Vec; use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; -use types::transaction::TransactionBundle; +use types::{boot::BootInfo, transaction::TransactionBundle}; use crate::DefaultSyscallHandler; -use crate::memory::Memory; +use crate::memory::{Memory, Perms}; use state::State; use vm::host_interface::NoopHost; -use vm::memory::{HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; +use vm::memory::{Mmu, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; use vm::registers::Register; use vm::vm::VM; @@ -77,7 +78,9 @@ impl Bootloader { } self.memory - .write_code(VirtualAddress(min_base as u32), &image); + .map_range(VirtualAddress(min_base as u32), core::cmp::max(image_size, 16 * 1024), Perms::rwx_kernel()); + self.memory + .write_bytes(VirtualAddress(min_base as u32), &image); (entry_point, self.memory.clone() as MmuRef) } @@ -98,6 +101,7 @@ impl Bootloader { self.place_bundle(&mut vm, bundle); let encoded_state = state.borrow().encode(); self.place_state(&mut vm, &encoded_state); + self.place_boot_info(&mut vm); vm.raw_run(); } @@ -121,4 +125,24 @@ impl Bootloader { (addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32, )); } + + fn place_boot_info(&mut self, vm: &mut VM) { + // For now the bootloader owns the page tables, so `root_ppn` is a placeholder (0). + let boot_info = BootInfo::new( + self.memory.current_root() as u32, + vm.memory.stack_top().as_u32(), + self.memory.size() as u32, + ); + let bytes = unsafe { + slice::from_raw_parts( + &boot_info as *const BootInfo as *const u8, + mem::size_of::(), + ) + }; + let addr = vm.set_reg_to_data(Register::A4, bytes); + vm.memory + .set_next_heap(VirtualAddress( + (addr as usize + bytes.len() + HEAP_PTR_OFFSET as usize) as u32, + )); + } } diff --git a/crates/bootloader/src/memory/memory.rs b/crates/bootloader/src/memory/memory.rs index a36b6d1..be65a9b 100644 --- a/crates/bootloader/src/memory/memory.rs +++ b/crates/bootloader/src/memory/memory.rs @@ -1,43 +1,11 @@ use std::cell::{Cell, Ref, RefCell}; use std::rc::Rc; -use vm::memory::{Mmu, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SIZE, PAGE_SHIFT}; +use vm::memory::{Mmu, Perms, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SHIFT}; use vm::metering::{MeterResult, Metering, MemoryAccessKind}; use crate::memory::pte::Pte; -#[derive(Clone, Copy, Debug)] -pub struct Perms { - /// Allow read access. - pub read: bool, - /// Allow write access. - pub write: bool, - /// Allow instruction fetch/execute. - pub exec: bool, - /// Mark page as user-accessible (false = kernel/supervisor-only). - pub user: bool, -} - -impl Perms { - pub fn rwx_kernel() -> Self { - Self { - read: true, - write: true, - exec: true, - user: false, - } - } - - pub fn rw_kernel() -> Self { - Self { - read: true, - write: true, - exec: false, - user: false, - } - } -} - /// Software Sv32 MMU backed by a contiguous physical buffer. /// /// Design at a glance: @@ -64,8 +32,10 @@ pub struct Memory { total_pages: usize, /// Contiguous physical backing store. backing: Rc>>, - /// Root L1 table (VPN1 index). - root: RefCell>, + /// Collection of root L1 tables (VPN1 index), one per address space. + root_tables: RefCell>>, + /// Index of the active root in `root_tables`. + current_root: Cell, /// Pool of L2 tables (VPN0 index). l2_tables: RefCell>>, /// Bump allocator for guest heap pointer. @@ -87,7 +57,8 @@ impl Memory { page_size, total_pages, backing: Rc::new(RefCell::new(vec![0u8; total])), - root: RefCell::new(Box::new([Pte::default(); 1024])), + root_tables: RefCell::new(vec![Box::new([Pte::default(); 1024])] ), + current_root: Cell::new(0), l2_tables: RefCell::new(Vec::new()), next_heap: Cell::new(VirtualAddress(0)), next_free_frame: Cell::new(0), @@ -98,6 +69,13 @@ impl Memory { self.backing.borrow().len() } + /// Allocate a fresh L1 root table and return its index. + pub fn allocate_root(&self) -> usize { + let mut roots = self.root_tables.borrow_mut(); + roots.push(Box::new([Pte::default(); 1024])); + roots.len() - 1 + } + /// Allocate a physical frame (4 KiB) and return its page number, or None if out of frames. fn allocate_frame(&self) -> Option { let frame = self.next_free_frame.get(); @@ -119,14 +97,18 @@ impl Memory { fn map_page(&self, va: VirtualAddress, perms: Perms) { let vpn1 = va.vpn1() as usize; let vpn0 = va.vpn0() as usize; - let mut root = self.root.borrow_mut(); + let mut roots = self.root_tables.borrow_mut(); + let current = self.current_root.get(); + let root = roots + .get_mut(current) + .unwrap_or_else(|| panic!("invalid root index {}", current)); if root[vpn1].next_l2.is_none() { let l2_idx = self.allocate_l2(); root[vpn1].next_l2 = Some(l2_idx); root[vpn1].valid = true; } let l2_idx = root[vpn1].next_l2.expect("l2 table missing"); - drop(root); + drop(roots); let mut l2s = self.l2_tables.borrow_mut(); let l2 = &mut l2s[l2_idx]; @@ -149,9 +131,10 @@ impl Memory { let mut remaining = len; while remaining > 0 { self.map_page(page_start, perms); - page_start = VirtualAddress(page_start.as_u32().wrapping_add(PAGE_SIZE as u32)); - if remaining > PAGE_SIZE { - remaining -= PAGE_SIZE; + page_start = + VirtualAddress(page_start.as_u32().wrapping_add(self.page_size as u32)); + if remaining > self.page_size { + remaining -= self.page_size; } else { remaining = 0; } @@ -163,7 +146,10 @@ impl Memory { let vpn1 = va.vpn1() as usize; let vpn0 = va.vpn0() as usize; let offset = va.offset() as usize; - let root: Ref<'_, Box<[Pte; 1024]>> = self.root.borrow(); + let roots = self.root_tables.borrow(); + let root = roots + .get(self.current_root.get()) + .unwrap_or_else(|| panic!("invalid root index {}", self.current_root.get())); let l2_idx = root.get(vpn1).and_then(|pte| pte.next_l2)?; let l2s = self.l2_tables.borrow(); let l2 = l2s.get(l2_idx)?; @@ -206,7 +192,7 @@ impl Memory { let phys = self .translate(va, kind) .expect("copy failed: unmapped virtual address"); - let page_remaining = PAGE_SIZE - (va.offset() as usize); + let page_remaining = self.page_size - (va.offset() as usize); let to_copy = core::cmp::min(page_remaining, remaining); { let mut backing = self.backing.borrow_mut(); @@ -220,6 +206,12 @@ impl Memory { va = VirtualAddress(va.as_u32().wrapping_add(to_copy as u32)); } } + + /// Write bytes to an already mapped virtual region without advancing the heap. + /// Callers must ensure the range is mapped and writable. + pub fn write_bytes(&self, start: VirtualAddress, data: &[u8]) { + self.copy_into_backing(start, data, MemoryAccessKind::Store); + } } impl Mmu for Memory { @@ -227,6 +219,22 @@ impl Mmu for Memory { self.backing.borrow() } + fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { + Memory::map_range(self, start, len, perms); + } + + fn set_root(&self, root: usize) { + let roots = self.root_tables.borrow(); + if root >= roots.len() { + panic!("set_root: invalid root index {}", root); + } + self.current_root.set(root); + } + + fn current_root(&self) -> usize { + self.current_root.get() + } + fn mem_slice( &self, start: VirtualAddress, @@ -368,16 +376,6 @@ impl Mmu for Memory { )) } - fn write_code(&self, start_addr: VirtualAddress, code: &[u8]) { - let end_addr = start_addr - .checked_add(code.len() as u32) - .expect("code write overflow"); - self.map_range(start_addr, code.len(), Perms::rwx_kernel()); - self.copy_into_backing(start_addr, code, MemoryAccessKind::Store); - self.next_heap - .set(VirtualAddress(end as u32 + HEAP_PTR_OFFSET)); - } - fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { let mut addr = self.next_heap.get().as_u32(); let align = 8; diff --git a/crates/bootloader/src/memory/mod.rs b/crates/bootloader/src/memory/mod.rs index 81428a1..3bb92bd 100644 --- a/crates/bootloader/src/memory/mod.rs +++ b/crates/bootloader/src/memory/mod.rs @@ -3,5 +3,6 @@ mod memory; mod pte; -pub use memory::{Memory, Perms}; +pub use memory::Memory; +pub use vm::memory::Perms; pub(crate) use pte::Pte; diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index 716ecfc..bbe6789 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -902,28 +902,32 @@ impl DefaultSyscallHandler { base.as_u32() } + /// Linux-like `munmap(2)` stub: accepts a VA/len pair and returns success. + /// Unmapping is not implemented yet, so this is a no-op placeholder. fn sys_munmap( &mut self, _args: [u32; 6], _memory: Memory, _metering: &mut dyn Metering, ) -> u32 { - // Not implemented yet; return success. 0 } + /// Linux-like `mprotect(2)` stub: ignores requested protections and returns success. + /// Permission changes are not tracked in this minimal MMU. fn sys_mprotect( &mut self, _args: [u32; 6], _memory: Memory, _metering: &mut dyn Metering, ) -> u32 { - // Not implemented yet; return success. 0 } + /// Minimal `brk(2)` implementation: + /// - a0 = new_break; if 0, return current break. + /// - Only moves the break forward; shrink requests are ignored. fn sys_brk(&mut self, _args: [u32; 6], _memory: Memory, _metering: &mut dyn Metering) -> u32 { - // a0 = new_break; if 0, return current break let new_brk = _args[0]; let current = _memory.next_heap().as_u32(); if new_brk == 0 { @@ -933,7 +937,6 @@ impl DefaultSyscallHandler { _memory.set_next_heap(VirtualAddress(new_brk)); new_brk } else { - // Do not shrink in this minimal implementation. current } } diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 6020e0e..47fdbba 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -2,3 +2,6 @@ pub mod config; pub use config::Config; +pub use types::boot::BootInfo; +pub mod task; +pub use task::{AddressSpace, Task, TrapFrame}; diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index b9b1763..120a211 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -6,17 +6,27 @@ extern crate alloc; use alloc::format; use core::mem::forget; use core::slice; -use kernel::Config; +use kernel::{BootInfo, Config, Task}; use program::{log, logf}; use types::transaction::{Transaction, TransactionBundle, TransactionType}; const SYSCALL_CREATE_ACCOUNT: u32 = 12; -/// Kernel entrypoint. Receives a pointer/length pair to an encoded `TransactionBundle` -/// (produced by the bootloader) and walks each transaction. State is managed via host -/// syscalls; no state blob is passed in. +#[allow(dead_code)] +static mut KERNEL_TASK: Option = None; + +/// Kernel entrypoint. Receives: +/// - `bundle_ptr`/`bundle_len`: encoded `TransactionBundle` prepared by the bootloader. +/// - `state_ptr`/`state_len`: optional state blob (currently unused). +/// - `boot_info_ptr`: bootloader handoff with stack + page-table root info. #[unsafe(no_mangle)] -pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) { +pub extern "C" fn _start( + bundle_ptr: *const u8, + bundle_len: usize, + _state_ptr: *const u8, + _state_len: usize, + boot_info_ptr: *const BootInfo, +) { // Copy args to locals before any syscalls (ecall clobbers a0). let bundle_ptr = bundle_ptr; let bundle_len = bundle_len; @@ -24,6 +34,21 @@ pub extern "C" fn _start(bundle_ptr: *const u8, bundle_len: usize) { log!("kernel boot"); logf!("bundle_len=%d", bundle_len as u32); + if let Some(info) = unsafe { boot_info_ptr.as_ref() } { + let task = Task::kernel(info.root_ppn, info.kstack_top); + unsafe { + KERNEL_TASK = Some(task); + } + logf!( + "boot_info: root_ppn=0x%x kstack_top=0x%x mem_size=%d", + info.root_ppn, + info.kstack_top, + info.memory_size + ); + } else { + log!("boot_info missing; kernel task not initialized"); + } + let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { diff --git a/crates/kernel/src/task.rs b/crates/kernel/src/task.rs new file mode 100644 index 0000000..f7b3833 --- /dev/null +++ b/crates/kernel/src/task.rs @@ -0,0 +1,64 @@ +use core::fmt; + +/// Minimal trapframe capturing user-visible registers on trap/return. +/// This mirrors RISC-V general-purpose regs plus PC. +#[derive(Clone, Copy, Default)] +pub struct TrapFrame { + /// General-purpose registers x0-x31 (x0 is always zero when restored). + pub regs: [u32; 32], + /// Program counter to resume at when returning to user. + pub pc: u32, +} + +impl fmt::Debug for TrapFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrapFrame") + .field("pc", &format_args!("0x{:08x}", self.pc)) + .finish() + } +} + +/// Describes a process/thread address space. +/// In a real kernel this would own the page table root PPN and ASID. +#[derive(Debug, Clone, Copy)] +pub struct AddressSpace { + /// Root page-table PPN (satp PPN field) for this address space. + pub root_ppn: u32, + /// Optional address-space identifier (ASID); zero if unused. + pub asid: u16, +} + +impl AddressSpace { + pub fn new(root_ppn: u32, asid: u16) -> Self { + Self { root_ppn, asid } + } +} + +/// Kernel-owned per-task state. This is where the kernel stores the +/// current address-space root and the saved trapframe. +#[derive(Debug)] +pub struct Task { + /// Saved user trapframe (regs + pc) to restore on return. + pub tf: TrapFrame, + /// Address space for this task (page-table root/asid). + pub addr_space: AddressSpace, + /// Kernel stack pointer for this task (top of the task's kernel stack). + pub kstack_top: u32, +} + +impl Task { + pub fn new(addr_space: AddressSpace, kstack_top: u32) -> Self { + Self { + tf: TrapFrame::default(), + addr_space, + kstack_top, + } + } + + /// Create the initial kernel task. This represents the supervisor itself: + /// - `root_ppn` is the kernel page-table root PPN that will be loaded into satp. + /// - `kstack_top` is the top of the kernel stack the kernel will run on. + pub fn kernel(root_ppn: u32, kstack_top: u32) -> Self { + Task::new(AddressSpace::new(root_ppn, 0), kstack_top) + } +} diff --git a/crates/types/src/boot.rs b/crates/types/src/boot.rs new file mode 100644 index 0000000..3cb8d6d --- /dev/null +++ b/crates/types/src/boot.rs @@ -0,0 +1,29 @@ +//! Boot-time handoff structures shared between bootloader and kernel. +//! +//! These types live in `types` so both sides agree on layout without +//! introducing circular dependencies. + +/// Minimal boot information passed from the bootloader to the kernel. +/// +/// Fields are kept simple and `#[repr(C)]` so the bootloader can write this +/// structure into guest memory and the kernel can read it back verbatim. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct BootInfo { + /// Root page-table physical page number to load into satp. + pub root_ppn: u32, + /// Top of the kernel stack. + pub kstack_top: u32, + /// Total physical memory size in bytes. + pub memory_size: u32, +} + +impl BootInfo { + pub const fn new(root_ppn: u32, kstack_top: u32, memory_size: u32) -> Self { + Self { + root_ppn, + kstack_top, + memory_size, + } + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index bad15fd..f7e8723 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -18,6 +18,9 @@ pub use primitives::*; pub mod transaction; pub use transaction::*; +pub mod boot; +pub use boot::BootInfo; + // used for serialization pub trait SerializeField { /// Appends `self` into `buf` at `*offset`, advancing the offset. diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory.rs index 6a5eb07..48ba6e0 100644 --- a/crates/vm/src/memory.rs +++ b/crates/vm/src/memory.rs @@ -9,6 +9,34 @@ pub const PAGE_SHIFT: u32 = 12; pub const VPN_MASK: u32 = 0x3ff; pub const PAGE_OFFSET_MASK: u32 = 0xfff; +/// Simple permission bits for page mappings (mirrors Sv32 R/W/X/U). +#[derive(Clone, Copy, Debug)] +pub struct Perms { + pub read: bool, + pub write: bool, + pub exec: bool, + pub user: bool, +} + +impl Perms { + pub const fn new(read: bool, write: bool, exec: bool, user: bool) -> Self { + Self { + read, + write, + exec, + user, + } + } + + pub fn rwx_kernel() -> Self { + Self::new(true, true, true, false) + } + + pub fn rw_kernel() -> Self { + Self::new(true, true, false, false) + } +} + /// Sv32 virtual address helper newtype. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct VirtualAddress(pub u32); @@ -66,6 +94,7 @@ impl From for usize { } pub trait Mmu: std::fmt::Debug { + // --- CPU-facing data access (loads/stores/fetches) --- fn mem(&self) -> Ref>; fn mem_slice(&self, start: VirtualAddress, end: VirtualAddress) -> Option>; fn store_u16(&self, addr: VirtualAddress, val: u16, metering: &mut dyn Metering, kind: MemoryAccessKind) -> bool; @@ -75,7 +104,10 @@ pub trait Mmu: std::fmt::Debug { fn load_byte(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_halfword(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_word(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; - fn write_code(&self, start_addr: VirtualAddress, code: &[u8]); + /// Set the current page-table root (index/identifier) used for translation. + fn set_root(&self, root: usize); + /// Get the current page-table root (index/identifier). + fn current_root(&self) -> usize; fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress; fn stack_top(&self) -> VirtualAddress; fn size(&self) -> usize; From da693c350f37f22ebc381159512f544f91c26abd Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 14:34:49 +0200 Subject: [PATCH 18/70] Fix kernel stack mapping and test harness lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map a dedicated 16 KiB stack region at the top of bootloader memory and start the heap after the loaded image to avoid overwriting kernel text/rodata. Expose map_range on the MMU trait and make range mapping robust to unaligned spans. Allow examples’ test runner to find kernel.elf by default, propagate verbose logging to the VM, and add bootloader bin to .gitignore. Note: crates/bootloader/bin/kernel.elf is a generated binary and currently marked modified. --- crates/bootloader/.gitignore | 1 + crates/bootloader/bin/kernel.abi.json | 7 ---- crates/bootloader/bin/kernel.elf | Bin 24764 -> 0 bytes crates/bootloader/bin/kernel_abi.rs | 30 ------------------ crates/bootloader/src/bootloader.rs | 19 +++++++++++ crates/bootloader/src/memory/memory.rs | 21 ++++++------ .../examples/tests/binary_comparison_test.rs | 3 +- crates/examples/tests/common/test_runner.rs | 13 ++++++-- crates/vm/src/memory.rs | 2 ++ crates/vm/src/vm.rs | 21 ------------ 10 files changed, 43 insertions(+), 74 deletions(-) create mode 100644 crates/bootloader/.gitignore delete mode 100644 crates/bootloader/bin/kernel.abi.json delete mode 100755 crates/bootloader/bin/kernel.elf delete mode 100644 crates/bootloader/bin/kernel_abi.rs diff --git a/crates/bootloader/.gitignore b/crates/bootloader/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/crates/bootloader/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/crates/bootloader/bin/kernel.abi.json b/crates/bootloader/bin/kernel.abi.json deleted file mode 100644 index d2293e3..0000000 --- a/crates/bootloader/bin/kernel.abi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": "1.0", - "functions": [ - ], - "events": [ - ] -} diff --git a/crates/bootloader/bin/kernel.elf b/crates/bootloader/bin/kernel.elf deleted file mode 100755 index 96a1cff296e26f7a751f3140cb4149df4ac8e5b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24764 zcmeHv3wTu3wf{Qj%uF66A$WL+iX)SRr|Ha`c~)<4av?l?wd9Lbz|a-pmP+mn0fT7t%x{ zf2~~r_ZVpTa^W{$T>dxKGk*TtGV^j<`Cr`EoWjpu$vq;I~?a=++W1~9NZ7#o`<`(SW{b!o`Bu@CUA^4 zZ_*m;wK{LIp`kh1QlF~RYU{N6e<@F;G!ZmZv^fo-&Jg zO=64v^!)nyP4iIH%Cwqos8D#9JC(qjD1 zA4W6<_nOAWhDJQI6VC@zh7%>c>4wGyiDtVy^P9c%7c8vP7HIX&&=zFbs5Q5sYbkG{ z-m7sf-dpBr_1@-rniu`l8tayDN1B?U-38v%{FJx8q4{J!=AfO!xR{Rx+Jc70C0;14 zK{e}dNLw1Vx=l14lMdLX8*wkjy{T@#s(I(5vBhX7(b(L?rUS7wd$oF~5P)t4Et;VF ztTIr{To@2jTP3{>phsh74tni1>%}8zYHss9?~hUTEY$Hd?qhH_Dba*ggFg^dLg7d> z7EdJAl$QPgbfpgZbfT_FYi?|T%^LG9vgkq>jEKA6?+^Hce#Ia1hy4+M)F1Q51O7lD z5DX}RP#_$L1fqdhARhDw1HoWW35J5w(Ke<%XuV(~awjHCHDs*a;roF6To+oCl!dlx3^ z=d0)!+Yu-3qnUbsd5PX^f`l57Aq&FWzRDq`zDY|y=74oVQWlt-3U1;{N0C{aiqz*5z%C_I> ztJqis85a8{2u14JqH!_?N^dz(*yr8oHv6$|rtI;fA1BM9qonw->{{y5i)Xl$L2Ih@ zL8Y`6&O=}JtE;1KdM!F$q9`1ST=bi(^a8gi7qULv!QWMa?k?)Bi^5T)xGxp-yY6yz zP|I8BN1p6ti=@?jsvO86?_ z5%Nd{&v)u>&_K?n4OP9V4s|Cp<<%~!TySMJZzq(s?C?ma zyok$Lb%oSX?ysC&=C@XveN&Axque(@Pi7gzR@=XW_HQ`3%tEWo6Vr?`rS92M`L<%I zLMX|MU0R&8Q#ZzjB{P&S@>N_=tj_uAmol5~>elnyA=4s~mxE?`p_fx;9U2IQ*9mj5AA4<9{-ZHLgouo`?2Zjk+5SKm3}n@&ZYgU^@j> zBo|ge-iM(-FWYpty!vcX=cTS!TOYeZX$uLNOTf!(*Ici!zFtu4ny*#gzUgwk?LR~{ zy=9jAddrpi@;`{`oL6S4Z#;QLwrvYZ+lOI{xk=htj`0?@#u;=nB2UoQwF%jM`$&0f z6DdXiOKNJ_Of~sGmvvlLyvsK!A^Ij%OKM3m`VHH07w7CpE_72zzWdp1+bgh@Lg=^H z_8)t6_no9RZxJyTFJqhUO;feoph<4IL|rs#nqE+ZeuQSgpGg>-l5gA>CH3{6PE}T? zgzV~OLAi68VDzJA5J^+dq0H2YBu%@BGSe<4Y5JEbGyNvY{M*+_n!K7alfOmM6+fWN z6;F|L<#Uv|@=qkq_$y^D`G};;2ML+W&k-_J69wtgiv)T2a?;m(1bM^?K_0o1l&@~4 zYy;6&>y~svir{x zl!Gc|556E|-+o7sUc8htFM_wHZzAcLuakUsfuMhLm7st7F4EVKAfK~ZkVmf|<-i0% zdGkMota=V9^*@5HR#WELZ;|xe4@i3cDbp9;zrtukzfTloIm7jkvVSLK54Tte@~KzKd4BSZ6I^;8_?@&7dcmCH-3Glpjyc5( zeL|OG_ClZUx_lKI!54hK83Qabew%ap7fhcw2)@KcWn`L00V|(hVS}3248_aSA4jQVL-R$S8^3C?E&Sn{?h46cpkF$L?%S9b3iJ9_k zMe3dIx8?O=(${IEEZ+`V@RRfHFTSO_CCv5PC4C)y?{N4CH*9IelN<{`e#>1Qcke$T zZQjw{QRwN)Yy#i8vbC(=BFs0fYT*+HrSU?sd{+@Q9KP}h$2&$`V91efM2y&lv4?T7 z6l3gO=;HzbF(PyfJ4Xzt{t@+Z>}U25ZMWvDt!|H8faiB$K4SmQGPB|)jvLi{%y%4H zxFMtExK??r7^-TC?kUVYxA|3=scfl`0ng~GkQu*?qzOwo{uSr85@I;FZ5M)NABVZ$R=hG>=Xxgz=t~v($~MBr zABP;aK^}PKUTT%Cy7E+6WnE8_dgxdAET>F4WU+1-=C5@w8T_e>*14pLDMopHxowxN zZ(=NxI?|2!2K9T_8vSwdZ}ZG@l*PYN^t<<}&_BXh$jw1I7t_x$^>Rao(EK9SW z?T0xQ$!w~D4iRIr?%UdCkC2JxlC)W>U|o4o7WtO#u)I#~r)?9m3wj1$68b7VwhS?6 zd9^xcyGz<_9UGgo*B>DGlLC(uNuJ74S>bd!(@5?JOY4(#(V7e+% zKXy#gcb5zL3efFycboY~k-wiF0gbY38SE7O{utRhZC~14L((Sb%ZD|Qp?}1}o~u~@ zpMsvNF8;^#T=m@ln|f~F_WzrnF-HFGtJnye*oL()k8}5RQTE%O9ckUebEzQ8d6?_V zHj40X@VD@JhL3v*zHcw$FD`5Qkg8$k+)i(*i#KBoz<(R_MXRXJgU>s<$@Ztk#$1@o zuksjtcsIhYJ5oNEWgw8?fL2_tGoTLrYx&oG1{mYf<7)qzwZ@&-U}pOCFa^m)q-!r7X{zA z1m@nGG50P-e08s&!{163Z7jEu*+s7s@5`undcL3!i;(&ox2U^GP&>Dj;!f&HS4Gyd z7XHXZnY=vQN09#AyiR@R2&|)yP{;5+UGPC=nQQhD=I=5kZ!PJ0R3hio;H~*XNEb0* zW3JG#523Dly->=Cq*h6yoH>iK%dW1IdVjF`)%RU$TFT2j za~4T2jG@d6L6YT{1$kzc^!MMFh2Kf?``kS zwk@OVifgL$qfyu~`qp|o#sT(@@=Y1ZC+Ahcg>vfMPAKTfjtazO= zpfmdYz5Gsn*GN+GN5X%PAn@9y?->i-2*TF1 zW2AJwO!~_i%D(?HWi!tR@!{C*+cR%2bw+^tmf2^SQf88vg?6zwYYR|6EV{AF$4N`wxOXqq|$K zZ7{I%&YOb%>+Wv-FDA~u^C9v-?3S;eZ|1)_UdVR64ZffoN$Sqt_6{lC?@;y|Z&J48 zF9JW$X1+mkNr})gR6345hxgb<3|SxDJzOs&QdT`#r4JiU`tE($>ncU83Lmix*WIwE z!a|k@o_k4Mznk=7g@XRaVS+vgeY*Zh%8bX{G_eNzU5K@cI=i!-s|4l9YC&DNPLe7H z3Hs`Dun&j16m#wY@=%B9hV8YwGZoKS&&~On_iT*)5%@LrA;h2HQE|_%-s%!yXCHJc zUt32?%UV(w)(qi#W!KPN^|DVyEG9}ApNJ(z#F8RnNdfbzkeN^|ND~oTPTWQkV&%-F zYMwtOj+ZcwNI$-7u>Qa}%Cdco^ik$JnA=xQC;6H8y7k{BNuJq+b?Z#ZzV|xG`MbKa zE8e4y(fOUqu|1^kSwY#OdnmhnKgsudgscb8Ud6o)@_|ivyuJG%$7n{}^~iSg)9mvZ z^U8BR#sK!3-jxvd81*48;8;MKig>3D{-wxc#R=;gIUXJX+nYIjk-qv;*j6bbUaSd=(8m(QycJ8)f2>mA2YB;_VGrO^$ z0-BD}nr_6Tc-D&jS~8yvulYOV-X*;{0r6O&klB0*_8Cz3ipRi59!X=Tp}&~>Mxx(m zZ-b7jVejw*queEO!P%s2uM_0(Fz65cY-tmewjZKj*mqb}1YcD4KHATh%74XcuY8t~ zGy!s%u*~35cei!hvdG@?eU=;c3NL}~3P~Pxiag4n?UBd1ZVz~c+%B>BfsD@LwIBGY zxkymL8%!Cve}MW{VXP0Lj`d|9N#*eChaN@$$;H0Hz=dwCc`?VVMGVezhy1h4)#(VBUQkb_id;4Eq7r+BQ?(2D|ile+(@L752{6Yytp8?SSD{{eSi1LxsAf)PVijfV`&Bp>@Psv^j;t`b<|6VPx zhh5FuZ}@I~9oA7V?8sM3JtcY(+i-ia-6v^M8~omO_=ndX#(4E+TYe7SJyLexc9M(T z7_(2Drr+&n=lt_p_fWP`rS0FVw-&%Z-|->*b68MX;(~f>jp6s%#xQmymQj!2Z!hI} zL@&a)IO|oHJRJMbHw+hGE0Q@M^vLt1`QSl!5&Jz~*+qE!zQj8m{f|#T zXILjgZ(EDO&s~4-RyuzUetv;@2xEOS=8NLd!d!nJbh|Q} zp>KoM)VJYpMw{~#uj3W>x}Ldei8WWYio8dLSF%O*I2^;Ej z|0g#V?%?-;Z2M1&nF{Ph7x0?cWB5y>f0&z9#Es^dt&-H`E>ZIR+G*DU#_vgcGiXyZ z>=r!0?)%PX9x;ztdlom!o;hxF{jmH7tYc{FSmNt8Qjq&ufQ1O^$6vOpojf}wgz)8 zuMymP5l>@pu5}aq-Ui5OFDZF@sGUf$oYz^iO_+lJXklmYF^*r8l{V{Cw80PCdQGWt4DB=KhE$T-*`jIElU%&CL ztf`dR)s=`59XL~t_0=Yf`%N_`J$LFI@d?Q;(6H{;^88zccU|kSPO8HEIR$tr`fTqH zk&DMN{JY3|a;Mwh<9uV!&a|~BPi@oQbMs-33o(SD+eb1UW$^bE`2H?fqHa*HSST>;GZV-w{Eb9A6_Az&vH`8;-WI zy)USaD!_X=5{_OM^c`?DPH^E-838~DbQq5f9b$D@$PI$T%tyP{rW9&lycW%U8 z$GY|9`PA_c)^}L1DR{2C{jjZJh`IJvZG{bCFSJ$0d?L5ykvZnuZv}7ccLXbr_y|1U z{likchq%{;^|~ADb@+^G3F~#dZ%iPry%}-sQpC0QN`{?03Vt7hJrzI(h1mc2Jla|H z>^)H&W|9Z3Udr$KqvzsxQPJ&s(KZ2RCg6XdV=wB%I>R>!aq*Z0_5g6NxEFf@HzOv7 zTo5Pl8p!fZmtYUsm^XPH)A3Nj3GnTLpLJ!*s$GbaT!@qKP784p-fJOFk}?ywiHMWL z%%o}&{d05w%-BA|{q5F0*h`r6YJS$U0c+(5W%G7pFZ+lnuYe8Zqpg+D!PEH|&&RMI zc^#gO#IuozEAqN{tSg>ZtK?gDbnC~%h`|q#x+dSFcI_dV%Z955xNm)Fm0Q8P?~|7_`3D(L!c&0`Iip zk96!S9FzrRrX(o&RGmElf4+F7ki9<-?@;HWZ|}iB_sGBF(Efw)v4;#BsDIUC+knD0 z0A1*o4fyCM_H#Lye`(czGz1`uUFN^J=nKCGFW~9YuUNSA#=2ayU%+JHZzSn z>h^Ty^nGGKWcZ|^U)F8MC{GvLqpkmr@G|844J;E}1WkvqSl%MI_&G4k(@bvqCGaHBUuEPE zp<}=)F~(*pqC8+eQ+lzXuVR{OVKcv&z6#9eN=^Ps=oc0?^GmQZK-3QY@XzS) zAz-#AGYzJAg|!)DGnLXzV3xm`hR`hFI>@9H4_JP!z`HOwPz}%sdJ35J zZ>F>8zk!R!qKuJ$0lfvx`ZtqIhk>^M-(chqrHc_a)S*5T52uyDr8zu|UIAu$GYzFZ zz;8JC5izit-b}*?OD7r%8Qc8&fw_J&4JQS7gQNUcfVn?r8bJx*-vZwWyB49vH5NAW zF%JUY0DW`)BD}<(ENteVO@9XF@o1(|v=_Jx{Mh;!gmv}?OIy z4g=BOfJ+Vg_E7=mLB?kCQ4w(GIM@sxh*U<`TG-4lquIdus2{`4sDBwS`$sdC(+XfO z@>#yzzsG^Api47}v;~;`r;WD)v%j?Q&w$xK+V~g1>fHWw7Pj+W0JhJubN+D)+xZ^>v;VSj7qESno$1G8ATze} zCjr}M+&TYR3)}g#f!Tl9_(ov+TszaRw6LAO8rVMX&iPMT*v|hcF#9tb{~Valz1#Sg z!0gX#{4_B86B};_9`C@rfG-5L$H)5?w&{-o^Z2&$hrm3ZZQKdWcE^o!+|-!33y(v z|6^$p@WM&9JjT%y2R)bXgglr&pW_R0ECmrOF#Q@sKI7j zzv{r8Zv?V7JHR+fm`j|0p#yVyzXNmrYzOB28y%SQS1`WNu5vuBa^y4p+g84*|M7H} zBcID}bzr9dnFDkF8xG9*2Y{_nO}n}*5HLSps-4@|a0{3XheQ5EC=p1-f+>8Z61SP{;qTOX z13{AmlO0qMO!$N0q>>1SRDUcHj}D-UsQsZ-t^J`C>Pe-eK}`)L(&0oZ74zrh_VKLr z-Nh*Mq%|(mf`L>+t@XPWLbD{t8cIVmF*WViB1$S0)ohNT%aGd8s43|M&Eabs=QnHd zTloK4U=&mFsZT^x(@`xF3##!@uVM?E8$-cVW5dGQ`SrC6@$s9+ayPV6S&8qk@Htjx zoYhB$7(;mbW6}*mS`&D8#7#42@WeO4VJlPi)}Os zwxfPEl1{2YEgpo3134A{gKY%-rfk%C32wz72&?I^Kalh%k`cdlQco14Ck{r9Z{7^< zdv{`rmTZ~JonS4X;<%!wqG?nbOojrfxZff5&rU>@ftWpN|1nMUC#A|jyw^xRjYkNx zM@ZET&>mC8mvYe@iFH71sa=fBp%0LQ)2k?&L373 zTlb$r)v0JA5z~U{IKKZ2M*PW7v1)%L6pp5%$pl73P)mM%)jg^S#Eh=j*5gCeTOeKA z<0O*`KJ5#Kl(>?NhI9RQI;!|o^D|McQCXNsL1<8g5{75BXK1WN@?+wvo>>FphgH>C zU8sNNdexW0hme6(LQNzBT0E)r_Q~wZ4M)Z16i%y=Xet#*;(JbX#@V>-_j|+{2tuAP z^=55uV}cD`4JQJzL^Kr&rlZMpDmP^M*2XEtgQmA!*r=uFFJ@jeL1EmxGwcW`u#CpULSxivtmj11pVTzJKNRu)4_|!>biX~H? zOYrTjk=?@IcRREe3MbO>STvHv7>uc*oJF4|cnM$_H#OHLwYiuJO{4DR=z4_~aWiD$ zdGG47$!5e7<4C2pp`QIgJROT*tcGLBbX0}U=}TyzBnk?}V+ua+4QXmJ6^54WdRepK zey#GdhFi48>Dpq4!0_d7%&#gDe!Bm7%x_+we!CX)OfN2V$W_IuJ?m zSJe0*Ta8x+LIWoa51bTH!ax~_bFWS|PRQ5#g8`?HgHJQe`dK3kl)1>j`w<;DsbZ5x z(&4zGAmENg(|&(4l}IaTEtpgTp=3CbOvWpN182anj?XsoWAppWM(IFR!sZhCbY|EA zHW@UUv?s#PZt55yJ2~s=cW@8fc*1=Q!cQ>i46p|Q!&c8M3TLge3eI5Kw~Ks&NF7B_ zaY_BJ?%{P$=a5dfMCsLB-z)h3Hz`c#-{b#NA%8j)2!>+8q?U~NgF*g786ho#$Tg7) zD_W(}Tk(Cj{YiEYSR$6%2E299!+Sg)iN#ZDIvrD@K?T#HriIe+L^>7@Xkq*{lFI1G zwSRmxn`7eB*oZJY_1QUEp1s)~{()_d{ubAE6QA<_LyzkEpVw>_KEqz}76Mr6;VKzS zD~Y&@7$}rZhwv9>f&mo^mo&^Q8cq7sl|7FB6Rbd4Z6Ciw(O1~dC9L6bdKWdYEr3s6 zSMP=SC$Fpb{y~od;wpA5)To+{1+jRJhO}@x6-9iI|1kY$*gwo-tri9Xn^YK@e!dfyEiz|^tAds~D za_G}p`I{}w^NTFhHAu;ZT|shv``|CRhnBIwsgb5J#`}Qz0S6T{wXc8d%DqYpPeI6O-58K8?`_(8BIjPAsA~ShIMhm zABv|#YOoT-sZ;+Abm14}f&(Rvoe6PYT;=$*TI@G43ZB|8vj(epzf%cJC)c}viD8t6 z7_G|tcV!_*?fp}S&$N1{YWlC<&`nOhC-tQN@o32Dl)@(LwLMcA!-i1|uTX;VkQzvZ ztxXX$56_ySfzm5yLTm_npfwwE9w;$pqk;2m7~?=|Hmqr&#DvYoQXwY3+)UzF^r_veC2Iz2bx=s91EUiV-cH|eQrvgzSPHU zN7}~)25`r*GMrN~&%ZGFTK|x#OhBv+J9P%Iy01eC%OdA+EqmHeHj0j?c-Kp|UMD5$-nK|2pXS zeC+RW@tIUR*>pp3ZpChw&&7U#D|eg|#5Ubr2OXb>Ey5XCmVupYy5}5pd`|X3>#Xdl zbOGqnZkNx?{vKCf{H=1(@j2NKaP`&hyAC=&FS~4nd4~4Xc4vc8yInp<+m7ohn+SKi z9~&KXe14YCu)<_=$)?-upyPA2BSGiSQT5b^Pa1RW@_E_mpyM;OcCzckr$RY8Zu8NP zq1)@AGtSSB>bt&+F)-}@@;TZYK-X74mOJQv3%V5O_>8Tc?4M6L==hxNVO*s)5$-l! z0nV)1?HcE8aezM52iZ@HoNA75b9t7Q-BG>{r+>VR+W4pZwbPn^N&N$KKd^p-lwkT^( Qp_@^IbARxIcCzXI52M^Da{vGU diff --git a/crates/bootloader/bin/kernel_abi.rs b/crates/bootloader/bin/kernel_abi.rs deleted file mode 100644 index 932d021b..0000000 --- a/crates/bootloader/bin/kernel_abi.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Auto-generated ABI client code -// DO NOT EDIT - Generated from ABI - -// Note: This code assumes the following imports in the parent file: -// use program::types::address::Address; -// use program::types::result::Result; -// use program::call::call; - -/// Client for interacting with KernelContract contract -pub struct KernelContract { - pub address: Address, -} - -impl KernelContract { - /// Create a new contract client - pub fn new(address: Address) -> Self { - Self { address } - } - - /// Call the main entry point directly (no routing) - pub fn call_main( - &self, - caller: &Address, - data: &[u8], - ) -> Option { - // Direct call without router encoding - call(caller, &self.address, data) - } - -} diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index e4f4dff..8c745ce 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -1,4 +1,5 @@ use core::{mem, slice}; +use core::fmt::Write as FmtWrite; use std::cell::RefCell; use std::rc::Rc; use std::vec::Vec; @@ -81,6 +82,18 @@ impl Bootloader { .map_range(VirtualAddress(min_base as u32), core::cmp::max(image_size, 16 * 1024), Perms::rwx_kernel()); self.memory .write_bytes(VirtualAddress(min_base as u32), &image); + // Start the heap after the loaded image to avoid overwriting kernel text/rodata + let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; + self.memory.set_next_heap(VirtualAddress(heap_start)); + // Ensure the kernel has a mapped stack region near the top of memory. + let stack_bytes = 4 * PAGE_SIZE; + let stack_base = self + .memory + .stack_top() + .as_usize() + .saturating_sub(stack_bytes); + self.memory + .map_range(VirtualAddress(stack_base as u32), stack_bytes, Perms::rw_kernel()); (entry_point, self.memory.clone() as MmuRef) } @@ -91,11 +104,17 @@ impl Bootloader { kernel_elf: &[u8], bundle: &TransactionBundle, state: Rc>, + verbose: bool, + verbose_writer: Option>>, ) { let (entry_point, memory) = self.load_kernel(kernel_elf); let host: Box = Box::new(NoopHost); let mut vm = VM::new(memory.clone(), host, Box::new(DefaultSyscallHandler::new(state.clone()))); + vm.cpu.verbose = verbose; + if let Some(writer) = verbose_writer { + vm.cpu.set_verbose_writer(writer); + } vm.cpu.pc = entry_point; self.place_bundle(&mut vm, bundle); diff --git a/crates/bootloader/src/memory/memory.rs b/crates/bootloader/src/memory/memory.rs index be65a9b..e2277d4 100644 --- a/crates/bootloader/src/memory/memory.rs +++ b/crates/bootloader/src/memory/memory.rs @@ -127,17 +127,16 @@ impl Memory { /// Map a contiguous virtual range page-by-page with the given permissions. pub fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { - let mut page_start = start.align_down(); - let mut remaining = len; - while remaining > 0 { - self.map_page(page_start, perms); - page_start = - VirtualAddress(page_start.as_u32().wrapping_add(self.page_size as u32)); - if remaining > self.page_size { - remaining -= self.page_size; - } else { - remaining = 0; - } + if len == 0 { + return; + } + let start_addr = start.align_down().as_usize(); + let end_addr = start.as_usize().saturating_add(len); + + let mut page_start = start_addr; + while page_start < end_addr { + self.map_page(VirtualAddress(page_start as u32), perms); + page_start = page_start.saturating_add(self.page_size); } } diff --git a/crates/examples/tests/binary_comparison_test.rs b/crates/examples/tests/binary_comparison_test.rs index 34c932a..82df208 100644 --- a/crates/examples/tests/binary_comparison_test.rs +++ b/crates/examples/tests/binary_comparison_test.rs @@ -49,8 +49,7 @@ fn test_vm_binary_comparison() -> Result<(), String> { // Create TestRunner with file output and verbose mode for instruction tracing let runner = TestRunner::with_writer(writer) .with_verbose(true) // Enable verbose mode for PC traces - .with_memory_size(512 * 1024) // Larger memory for crypto-heavy binaries - .with_max_pages(128); + .with_memory_size(512 * 1024); // Larger memory for crypto-heavy binaries // Run all test cases runner.execute()?; diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index c5c5ca1..5d19a1f 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -3,7 +3,7 @@ use std::env; use std::fs::{self, File}; use std::io::Write as IoWrite; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::rc::Rc; use core::cell::RefCell; @@ -155,6 +155,12 @@ impl TestRunner { })?, &case.bundle, state, + self.verbose, + if self.verbose { + Some(self.writer.clone()) + } else { + None + }, ); // For now we treat successful bootloader execution as a passed test. @@ -170,8 +176,9 @@ impl Default for TestRunner { impl TestRunner { fn load_kernel_from_env() -> Option> { - let path = - env::var("KERNEL_ELF").unwrap_or_else(|_| "crates/bootloader/bin/kernel.elf".to_string()); + let path = env::var("KERNEL_ELF") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bootloader/bin/kernel.elf")); fs::read(&path).ok() } } diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory.rs index 48ba6e0..57d2419 100644 --- a/crates/vm/src/memory.rs +++ b/crates/vm/src/memory.rs @@ -104,6 +104,8 @@ pub trait Mmu: std::fmt::Debug { fn load_byte(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_halfword(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_word(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; + /// Map a virtual range with the provided permissions. + fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms); /// Set the current page-table root (index/identifier) used for translation. fn set_root(&self, root: usize); /// Get the current page-table root (index/identifier). diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index a339b6d..5db5d68 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -62,27 +62,6 @@ impl VM { self.cpu.set_metering(metering); } - /// Loads program code into memory and sets the starting address. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how programs are loaded into - /// a VM. In real systems, this would involve loading from disk, parsing - /// executable formats, and setting up memory protection. - /// - /// PARAMETERS: - /// - alloc_add: The address where the code should be allocated in memory - /// - start_addr: Where the program should start executing from - /// - code: The binary program code to load - /// - /// MEMORY LAYOUT: Programs are typically loaded at specific addresses - /// to ensure proper alignment and to avoid conflicts with system memory. - pub fn set_code(&mut self, alloc_add: u32, start_addr: u32, code: &[u8]) { - // EDUCATIONAL: Write the program code to memory starting at address 0 - self.memory.write_code(VirtualAddress(alloc_add), code); - - // EDUCATIONAL: Set the program counter to the starting address - self.cpu.pc = start_addr; - } - /// Allocates memory on the heap and writes data to it. /// /// EDUCATIONAL PURPOSE: This demonstrates dynamic memory allocation in a VM. From 37ab73d8f888adb1bec352f2c03e3bebb2c0e930 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 15:08:46 +0200 Subject: [PATCH 19/70] examples compile and run --- Cargo.lock | 1 + Makefile | 2 +- crates/bootloader/src/bootloader.rs | 10 ++-- crates/kernel/Cargo.toml | 1 + crates/kernel/src/global.rs | 24 +++++++++ crates/kernel/src/main.rs | 83 +++++++++++++++-------------- 6 files changed, 76 insertions(+), 45 deletions(-) create mode 100644 crates/kernel/src/global.rs diff --git a/Cargo.lock b/Cargo.lock index 3e9bab4..823e8a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,6 +250,7 @@ name = "kernel" version = "0.1.0" dependencies = [ "program", + "state", "types", ] diff --git a/Makefile b/Makefile index 4e82ab7..1366803 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ run_examples: @mkdir -p crates/bootloader/bin @$(AVM32) all --bin kernel --manifest-path crates/kernel/Cargo.toml --features guest_kernel --out-dir crates/bootloader/bin --src crates/kernel/src/main.rs @echo "=== Running example crate tests ===" - cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../bootloader/bin/kernel.elf" cargo test test_examples -- --nocapture + cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../bootloader/bin/kernel.elf" cargo test --test examples_test -- --nocapture @echo "=== Example programs build and tests complete ===" clean: diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 8c745ce..b114bf2 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -16,6 +16,9 @@ use vm::memory::{Mmu, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SI use vm::registers::Register; use vm::vm::VM; +const MIN_KERNEL_MAP_BYTES: usize = 16 * 1024; +const KERNEL_STACK_BYTES: usize = 4 * PAGE_SIZE; + /// Boot configuration options consumed by the loader. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BootConfig { @@ -79,21 +82,20 @@ impl Bootloader { } self.memory - .map_range(VirtualAddress(min_base as u32), core::cmp::max(image_size, 16 * 1024), Perms::rwx_kernel()); + .map_range(VirtualAddress(min_base as u32), core::cmp::max(image_size, MIN_KERNEL_MAP_BYTES), Perms::rwx_kernel()); self.memory .write_bytes(VirtualAddress(min_base as u32), &image); // Start the heap after the loaded image to avoid overwriting kernel text/rodata let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; self.memory.set_next_heap(VirtualAddress(heap_start)); // Ensure the kernel has a mapped stack region near the top of memory. - let stack_bytes = 4 * PAGE_SIZE; let stack_base = self .memory .stack_top() .as_usize() - .saturating_sub(stack_bytes); + .saturating_sub(KERNEL_STACK_BYTES); self.memory - .map_range(VirtualAddress(stack_base as u32), stack_bytes, Perms::rw_kernel()); + .map_range(VirtualAddress(stack_base as u32), KERNEL_STACK_BYTES, Perms::rw_kernel()); (entry_point, self.memory.clone() as MmuRef) } diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 6092861..7c1d57a 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -13,6 +13,7 @@ guest_kernel = [] [dependencies] program = { path = "../program" } types = { path = "../types" } +state = { path = "../state" } [[bin]] name = "kernel" diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs new file mode 100644 index 0000000..83176ab --- /dev/null +++ b/crates/kernel/src/global.rs @@ -0,0 +1,24 @@ +use core::cell::UnsafeCell; + +/// Minimal wrapper to store non-`Sync` types in statics. +/// +/// Safety: Callers must guarantee exclusive access when mutating. +pub struct Global { + inner: UnsafeCell, +} + +impl Global { + pub const fn new(value: T) -> Self { + Self { + inner: UnsafeCell::new(value), + } + } + + /// # Safety + /// Callers must ensure exclusive access or otherwise serialize mutations. + pub unsafe fn get_mut(&self) -> &mut T { + &mut *self.inner.get() + } +} + +unsafe impl Sync for Global {} diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 120a211..5a40b2a 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -3,17 +3,23 @@ extern crate alloc; -use alloc::format; +use alloc::{format, vec, vec::Vec}; use core::mem::forget; use core::slice; use kernel::{BootInfo, Config, Task}; use program::{log, logf}; +use state::State; use types::transaction::{Transaction, TransactionBundle, TransactionType}; -const SYSCALL_CREATE_ACCOUNT: u32 = 12; +#[allow(dead_code)] +const KERNEL_TASK_IDX: usize = 0; + +mod global; +use global::Global; #[allow(dead_code)] -static mut KERNEL_TASK: Option = None; +static TASKS: Global>> = Global::new(None); +static STATE: Global> = Global::new(None); /// Kernel entrypoint. Receives: /// - `bundle_ptr`/`bundle_len`: encoded `TransactionBundle` prepared by the bootloader. @@ -23,21 +29,38 @@ static mut KERNEL_TASK: Option = None; pub extern "C" fn _start( bundle_ptr: *const u8, bundle_len: usize, - _state_ptr: *const u8, - _state_len: usize, + state_ptr: *const u8, + state_len: usize, boot_info_ptr: *const BootInfo, ) { - // Copy args to locals before any syscalls (ecall clobbers a0). - let bundle_ptr = bundle_ptr; - let bundle_len = bundle_len; - log!("kernel boot"); logf!("bundle_len=%d", bundle_len as u32); + // Initialize state from provided blob if present. + unsafe { + let state_slot = STATE.get_mut(); + if !state_ptr.is_null() && state_len > 0 { + let bytes = slice::from_raw_parts(state_ptr, state_len); + *state_slot = State::decode(bytes).or_else(|| { + log!("state decode failed; starting empty state"); + Some(State::new()) + }); + if state_slot.is_some() { + logf!("state initialized (len=%d)", state_len as u32); + } + } else { + *state_slot = Some(State::new()); + } + } + if let Some(info) = unsafe { boot_info_ptr.as_ref() } { let task = Task::kernel(info.root_ppn, info.kstack_top); unsafe { - KERNEL_TASK = Some(task); + let tasks_slot = TASKS.get_mut(); + match tasks_slot { + Some(tasks) => tasks.push(task), + None => *tasks_slot = Some(vec![task]), + } } logf!( "boot_info: root_ppn=0x%x kstack_top=0x%x mem_size=%d", @@ -98,36 +121,16 @@ fn create_account(tx: &Transaction) { ); } - let addr_ptr = tx.to.0.as_ptr(); - let code_ptr = tx.data.as_ptr(); - let code_len = tx.data.len(); - - #[cfg(target_arch = "riscv32")] - unsafe { - let mut result: u32; - core::arch::asm!( - "li a7, {create}", - "mv a1, {addr}", - "mv a2, {code_ptr}", - "mv a3, {code_len}", - "ecall", - "mv {out}, a0", - create = const SYSCALL_CREATE_ACCOUNT, - addr = in(reg) addr_ptr, - code_ptr = in(reg) code_ptr, - code_len = in(reg) code_len, - out = lateout(reg) result, - ); - if result == 0 { - log!("account created via syscall"); - } else { - log!("account creation failed via syscall"); - } - } - #[cfg(not(target_arch = "riscv32"))] - { - log!("(host) account creation syscall skipped (not riscv32)"); - } + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = state.get_account_mut(&tx.to); + account.code = tx.data.clone(); + account.is_contract = is_contract; + let msg = format!( + "account created in kernel state: addr={} is_contract={} code_len={}", + tx.to, is_contract, code_size + ); + let msg_ref: &str = msg.as_str(); + log!(msg_ref); } #[inline(never)] From 4299be3244a0a29d21994b9ffe4acb0f1edd0ee6 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 19:11:47 +0200 Subject: [PATCH 20/70] launch WIP --- Cargo.lock | 1 - crates/examples/Cargo.toml | 1 - crates/examples/tests/common/utils.rs | 11 ---- crates/kernel/src/launch.rs | 73 +++++++++++++++++++++++++ crates/kernel/src/lib.rs | 2 + crates/kernel/src/main.rs | 79 +++++++++++++++++++++++++-- 6 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 crates/kernel/src/launch.rs diff --git a/Cargo.lock b/Cargo.lock index 823e8a6..843775e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,7 +160,6 @@ dependencies = [ "bootloader", "compiler", "k256", - "kernel", "once_cell", "program", "serde_json", diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 61b8cea..b992162 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -13,7 +13,6 @@ types = { path = "../types" } # adjust path as needed compiler = { path = "../compiler" } # adjust path as needed state = { path = "../state" } bootloader = { path = "../bootloader" } -kernel = { path = "../kernel" } once_cell = "1.19.0" serde_json = "1.0" diff --git a/crates/examples/tests/common/utils.rs b/crates/examples/tests/common/utils.rs index d28fecc..10ae255 100644 --- a/crates/examples/tests/common/utils.rs +++ b/crates/examples/tests/common/utils.rs @@ -1,4 +1,3 @@ -use kernel::Config; use compiler::elf::parse_elf_from_bytes; use compiler::{EventAbi, EventParam, ParamType}; use serde_json::Value; @@ -128,16 +127,6 @@ pub fn get_program_code(name: &str) -> Vec { .get_flat_rodata() .unwrap_or_else(|| (vec![], usize::MAX as u64)); - // assert sizes - assert!( - code.len() <= Config::CODE_SIZE_LIMIT, - "code size exceeds limit" - ); - assert!( - rodata.len() <= Config::RO_DATA_SIZE_LIMIT, - "read only data size exceeds limit" - ); - let mut total_len = code_start + code.len() as u64; // assumes rodata is after code if rodata.len() > 0 { total_len = rodata_start + rodata.len() as u64; // assumes rodata is after code diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs new file mode 100644 index 0000000..1afd784 --- /dev/null +++ b/crates/kernel/src/launch.rs @@ -0,0 +1,73 @@ +#![allow(dead_code)] + +use crate::{AddressSpace, Config, Task}; +use program::logf; + +// Linux-style protection and mapping flags (subset). +const PROT_READ: u32 = 0x1; +const PROT_WRITE: u32 = 0x2; +const PROT_EXEC: u32 = 0x4; + +const MAP_PRIVATE: u32 = 0x02; +const MAP_ANON: u32 = 0x20; +const MAP_FIXED: u32 = 0x10; + +const PAGE_SIZE: usize = 4096; +const STACK_BYTES: usize = 0x4000; // 16 KiB user stack +const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap +pub const PROGRAM_VA_BASE: u32 = 0x0; + +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +/// Total mapped window for a program: code/rodata, stack, and heap. +pub const PROGRAM_WINDOW_BYTES: usize = align_up( + Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, + PAGE_SIZE, +); + +fn syscall_mmap(addr: u32, len: usize, prot: u32, flags: u32) -> u32 { + let ret: u32; + unsafe { + core::arch::asm!( + "ecall", + in("a7") 222u32, // __NR_mmap + inlateout("a0") addr => ret, + in("a1") len as u32, + in("a2") prot, + in("a3") flags, + in("a4") 0u32, // fd + in("a5") 0u32, // offset + ); + } + ret +} + +/// Create a new task for a program and map its virtual address window via syscalls. +/// +/// This sets up: +/// - Maps a fixed VA window [PROGRAM_VA_BASE, PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES). +/// - Returns a Task with the new address space and provided kernel stack top. +/// +/// The caller is responsible for copying program bytes into the mapped window +/// and initializing the user trapframe (PC/SP/args) before running. +pub fn launch_program(asid: u16, kstack_top: u32) -> Option { + let prot = PROT_READ | PROT_WRITE | PROT_EXEC; + let flags = MAP_PRIVATE | MAP_ANON | MAP_FIXED; + let addr = syscall_mmap(PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, prot, flags); + if addr != PROGRAM_VA_BASE { + logf!("launch_program: mmap failed"); + return None; + } + + // Root/asid tracking is minimal; real satp handling is TBD. + let task = Task::new(AddressSpace::new(0, asid), kstack_top); + logf!( + "launch_program: asid=%d base=0x%x size=%d", + asid as u32, + PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES as u32 + ); + Some(task) +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 47fdbba..47d3249 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -5,3 +5,5 @@ pub use config::Config; pub use types::boot::BootInfo; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; +pub mod launch; +pub use launch::{launch_program, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 5a40b2a..1536590 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -5,21 +5,23 @@ extern crate alloc; use alloc::{format, vec, vec::Vec}; use core::mem::forget; +use core::ptr; use core::slice; -use kernel::{BootInfo, Config, Task}; +use kernel::{BootInfo, Config, Task, launch_program, PROGRAM_WINDOW_BYTES}; use program::{log, logf}; use state::State; use types::transaction::{Transaction, TransactionBundle, TransactionType}; +mod global; +use crate::global::Global; + #[allow(dead_code)] const KERNEL_TASK_IDX: usize = 0; -mod global; -use global::Global; - #[allow(dead_code)] static TASKS: Global>> = Global::new(None); static STATE: Global> = Global::new(None); +static BOOT_INFO: Global> = Global::new(None); /// Kernel entrypoint. Receives: /// - `bundle_ptr`/`bundle_len`: encoded `TransactionBundle` prepared by the bootloader. @@ -54,6 +56,9 @@ pub extern "C" fn _start( } if let Some(info) = unsafe { boot_info_ptr.as_ref() } { + unsafe { + *BOOT_INFO.get_mut() = Some(*info); + } let task = Task::kernel(info.root_ppn, info.kstack_top); unsafe { let tasks_slot = TASKS.get_mut(); @@ -98,6 +103,7 @@ pub extern "C" fn _start( fn execute_transaction(tx: &Transaction) { match tx.tx_type { TransactionType::CreateAccount => create_account(tx), + TransactionType::ProgramCall => program_call(tx), _ => log!("executing transaction"), } } @@ -133,6 +139,71 @@ fn create_account(tx: &Transaction) { log!(msg_ref); } +fn program_call(tx: &Transaction) { + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = match state.get_account(&tx.to) { + Some(acc) => acc, + None => { + logf!( + "%s", + display: format!("Program call failed: account {} does not exist", tx.to) + ); + return; + } + }; + + if !account.is_contract { + logf!( + "%s", + display: format!( + "Program call failed: target {} is not a contract (code_len={})", + tx.to, + account.code.len() + ) + ); + return; + } + + let code_len = account.code.len(); + let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + if code_len > max { + panic!( + "❌ Program call rejected: code size ({}) exceeds limit ({})", + code_len, max + ); + } + + logf!( + "%s", + display: format!( + "Program call: from={} to={} input_len={} code_len={}", + tx.from, + tx.to, + tx.data.len(), + code_len + ) + ); + + let kstack_top = unsafe { BOOT_INFO.get_mut().as_ref().map(|b| b.kstack_top).unwrap_or(0) }; + + if let Some(task) = launch_program(0, kstack_top) { + logf!( + "Program task created: root=0x%x window_size=%d", + task.addr_space.root_ppn, + PROGRAM_WINDOW_BYTES as u32 + ); + unsafe { + let tasks_slot = TASKS.get_mut(); + match tasks_slot { + Some(tasks) => tasks.push(task), + None => *tasks_slot = Some(vec![task]), + } + } + } else { + log!("Program call skipped: no memory manager installed"); + } +} + #[inline(never)] fn halt() -> ! { unsafe { core::arch::asm!("ebreak") }; From fda41c91eee7062298654661ba6f5e62066f7c28 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 22 Dec 2025 23:06:51 +0200 Subject: [PATCH 21/70] WIP kernel MMU --- crates/bootloader/src/bootloader.rs | 13 +- crates/bootloader/src/memory/memory.rs | 302 +++++++++++++++++-------- crates/kernel/src/config.rs | 2 - crates/kernel/src/global.rs | 2 +- crates/kernel/src/launch.rs | 45 +--- crates/kernel/src/lib.rs | 2 + crates/kernel/src/main.rs | 9 +- crates/kernel/src/mmu.rs | 170 ++++++++++++++ crates/types/src/boot.rs | 5 +- crates/vm/src/cpu.rs | 2 + crates/vm/src/exe.rs | 8 +- crates/vm/src/memory.rs | 10 +- crates/vm/src/vm.rs | 57 +---- 13 files changed, 425 insertions(+), 202 deletions(-) create mode 100644 crates/kernel/src/mmu.rs diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index b114bf2..57e3542 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -128,7 +128,7 @@ impl Bootloader { fn place_bundle(&mut self, vm: &mut VM, bundle: &TransactionBundle) { let encoded = bundle.encode(); - let addr = vm.set_reg_to_data(Register::A0, &encoded); + let addr = self.place_data(vm, Register::A0, &encoded); // Register a length hint so the kernel can bounds-check the payload. vm.set_reg_u32(Register::A1, encoded.len() as u32); // Keep heap aligned after our write. @@ -139,7 +139,7 @@ impl Bootloader { } fn place_state(&mut self, vm: &mut VM, state: &[u8]) { - let addr = vm.set_reg_to_data(Register::A2, state); + let addr = self.place_data(vm, Register::A2, state); vm.set_reg_u32(Register::A3, state.len() as u32); vm.memory .set_next_heap(VirtualAddress( @@ -153,6 +153,7 @@ impl Bootloader { self.memory.current_root() as u32, vm.memory.stack_top().as_u32(), self.memory.size() as u32, + self.memory.next_free_ppn() as u32, ); let bytes = unsafe { slice::from_raw_parts( @@ -160,10 +161,16 @@ impl Bootloader { mem::size_of::(), ) }; - let addr = vm.set_reg_to_data(Register::A4, bytes); + let addr = self.place_data(vm, Register::A4, bytes); vm.memory .set_next_heap(VirtualAddress( (addr as usize + bytes.len() + HEAP_PTR_OFFSET as usize) as u32, )); } + + fn place_data(&self, vm: &mut VM, reg: Register, data: &[u8]) -> u32 { + let addr = self.memory.alloc_on_heap(data).as_u32(); + vm.cpu.regs[reg as usize] = addr; + addr + } } diff --git a/crates/bootloader/src/memory/memory.rs b/crates/bootloader/src/memory/memory.rs index e2277d4..f484edd 100644 --- a/crates/bootloader/src/memory/memory.rs +++ b/crates/bootloader/src/memory/memory.rs @@ -1,23 +1,31 @@ use std::cell::{Cell, Ref, RefCell}; +use std::collections::HashMap; use std::rc::Rc; -use vm::memory::{Mmu, Perms, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SHIFT}; +use vm::memory::{Mmu, Perms, VirtualAddress}; use vm::metering::{MeterResult, Metering, MemoryAccessKind}; -use crate::memory::pte::Pte; +// Minimal Sv32 bit layout for guest-visible PTEs. +const PTE_V: u32 = 1 << 0; +const PTE_R: u32 = 1 << 1; +const PTE_W: u32 = 1 << 2; +const PTE_X: u32 = 1 << 3; +const PTE_U: u32 = 1 << 4; + +// Sv32 satp PPN mask (bits 21:0). Mode bits are ignored in this emulator. +const SATP_PPN_MASK: u32 = 0x003f_ffff; /// Software Sv32 MMU backed by a contiguous physical buffer. /// /// Design at a glance: /// - Physical memory is a single `Vec` (`backing`). Frames are 4 KiB slices into it. /// - Virtual→physical is resolved with Sv32-style page tables: L1 root (VPN1) and L2 (VPN0). -/// - Page tables themselves live in host memory (`root` + `l2_tables`) and store simple PTEs(page table entry). +/// - Page tables live in guest memory; `translate` walks them using the satp root PPN. /// - A bump frame allocator hands out PPNs (physical page numbers) sequentially from the backing; no free list yet. /// - Mapping APIs (`map_page`/`map_range`) allocate tables/frames and set R/W/X/U bits. /// - `translate` walks VPN1→VPN0, checks permissions against the access kind, and returns a byte /// offset into the backing. All loads/stores go through this path. -/// - A guest heap bump pointer (`next_heap`) drives simple allocations; writes copy into backing -/// via translation to respect page boundaries. +/// - A guest heap bump pointer is tracked per-root. /// /// Limitations/assumptions: /// - No unmap or reuse of frames yet; the allocator only grows. @@ -32,14 +40,10 @@ pub struct Memory { total_pages: usize, /// Contiguous physical backing store. backing: Rc>>, - /// Collection of root L1 tables (VPN1 index), one per address space. - root_tables: RefCell>>, - /// Index of the active root in `root_tables`. - current_root: Cell, - /// Pool of L2 tables (VPN0 index). - l2_tables: RefCell>>, - /// Bump allocator for guest heap pointer. - next_heap: Cell, + /// satp value that selects the active root PPN. + satp: Cell, + /// Per-root heap bump pointer (VA). + per_root_heap: RefCell>, /// Next free physical frame index for frame allocation. next_free_frame: Cell, } @@ -53,15 +57,15 @@ impl Memory { let total = total_pages .checked_mul(page_size) .expect("physical memory size overflow"); + // Reserve frame 0 for the initial root page table. + let root_ppn: usize = 0; Self { page_size, total_pages, backing: Rc::new(RefCell::new(vec![0u8; total])), - root_tables: RefCell::new(vec![Box::new([Pte::default(); 1024])] ), - current_root: Cell::new(0), - l2_tables: RefCell::new(Vec::new()), - next_heap: Cell::new(VirtualAddress(0)), - next_free_frame: Cell::new(0), + satp: Cell::new(root_ppn as u32), + per_root_heap: RefCell::new(HashMap::new()), + next_free_frame: Cell::new(root_ppn + 1), } } @@ -69,11 +73,17 @@ impl Memory { self.backing.borrow().len() } - /// Allocate a fresh L1 root table and return its index. - pub fn allocate_root(&self) -> usize { - let mut roots = self.root_tables.borrow_mut(); - roots.push(Box::new([Pte::default(); 1024])); - roots.len() - 1 + fn root_ppn(&self) -> usize { + (self.satp.get() & SATP_PPN_MASK) as usize + } + + fn root_base(&self) -> Option { + let base = self.root_ppn().checked_mul(self.page_size)?; + if base + self.page_size > self.total_size() { + None + } else { + Some(base) + } } /// Allocate a physical frame (4 KiB) and return its page number, or None if out of frames. @@ -86,43 +96,100 @@ impl Memory { Some(frame) } - /// Allocate a fresh L2 table and return its index in the L2 pool. - fn allocate_l2(&self) -> usize { - let mut l2s = self.l2_tables.borrow_mut(); - l2s.push(Box::new([Pte::default(); 1024])); - l2s.len() - 1 + pub fn next_free_ppn(&self) -> usize { + self.next_free_frame.get() + } + + fn zero_frame(&self, ppn: usize) { + let mut backing = self.backing.borrow_mut(); + let start = ppn + .checked_mul(self.page_size) + .expect("frame offset overflow"); + let end = start + self.page_size; + backing[start..end].fill(0); + } + + fn read_pte(&self, phys_addr: usize) -> Option { + let backing = self.backing.borrow(); + let end = phys_addr.checked_add(4)?; + if end > backing.len() { + return None; + } + Some(u32::from_le_bytes( + backing[phys_addr..end].try_into().unwrap(), + )) + } + + fn write_pte(&self, phys_addr: usize, val: u32) { + let mut backing = self.backing.borrow_mut(); + let end = phys_addr + .checked_add(4) + .expect("pte write offset overflow"); + if end > backing.len() { + panic!("pte write out of bounds"); + } + backing[phys_addr..end].copy_from_slice(&val.to_le_bytes()); } /// Map a single 4 KiB page at `va` with the given permissions, allocating tables/frames as needed. fn map_page(&self, va: VirtualAddress, perms: Perms) { + let root_base = self.root_base().expect("invalid root page table base"); let vpn1 = va.vpn1() as usize; let vpn0 = va.vpn0() as usize; - let mut roots = self.root_tables.borrow_mut(); - let current = self.current_root.get(); - let root = roots - .get_mut(current) - .unwrap_or_else(|| panic!("invalid root index {}", current)); - if root[vpn1].next_l2.is_none() { - let l2_idx = self.allocate_l2(); - root[vpn1].next_l2 = Some(l2_idx); - root[vpn1].valid = true; + + // Ensure L2 table exists. + let root_pte_addr = root_base + vpn1 * core::mem::size_of::(); + let root_pte = self.read_pte(root_pte_addr).unwrap_or(0); + let root_is_valid = root_pte & PTE_V != 0; + let root_has_perms = root_pte & (PTE_R | PTE_W | PTE_X) != 0; + + if root_is_valid && root_has_perms { + panic!("superpages not supported"); } - let l2_idx = root[vpn1].next_l2.expect("l2 table missing"); - drop(roots); - let mut l2s = self.l2_tables.borrow_mut(); - let l2 = &mut l2s[l2_idx]; - if !l2[vpn0].valid { - let frame = self + let l2_ppn = if root_is_valid { + (root_pte >> 10) as usize + } else { + let l2_frame = self .allocate_frame() - .expect("out of physical frames while mapping"); - l2[vpn0].ppn = frame; - l2[vpn0].valid = true; - l2[vpn0].read = perms.read; - l2[vpn0].write = perms.write; - l2[vpn0].exec = perms.exec; - l2[vpn0].user = perms.user; + .expect("out of physical frames while mapping L2"); + self.zero_frame(l2_frame); + let new_pte = ((l2_frame as u32) << 10) | PTE_V; + self.write_pte(root_pte_addr, new_pte); + l2_frame + }; + + let l2_base = l2_ppn + .checked_mul(self.page_size) + .expect("l2 base overflow"); + let leaf_addr = l2_base + vpn0 * core::mem::size_of::(); + + if let Some(existing) = self.read_pte(leaf_addr) { + if existing & PTE_V != 0 { + return; + } + } + + let frame = self + .allocate_frame() + .expect("out of physical frames while mapping leaf"); + self.zero_frame(frame); + + let mut flags = PTE_V; + if perms.read { + flags |= PTE_R; + } + if perms.write { + flags |= PTE_W; } + if perms.exec { + flags |= PTE_X; + } + if perms.user { + flags |= PTE_U; + } + let leaf_pte = ((frame as u32) << 10) | flags; + self.write_pte(leaf_addr, leaf_pte); } /// Map a contiguous virtual range page-by-page with the given permissions. @@ -141,33 +208,55 @@ impl Memory { } /// Translate a virtual address to a physical offset into `backing`, checking permissions. + /// + /// This emulates an Sv32 page-table walk driven by the current `satp`: + /// - `satp` PPN selects the root L1 page table (written by the kernel in guest memory). + /// - We read the L1 PTE at VPN1; it must be valid and non-leaf (no superpages here). + /// - From that PPN we read the L2 PTE at VPN0; it must be valid and carry R/W/X bits. + /// - Permissions are checked against the access kind; on success we return a byte offset + /// into the physical backing buffer. + /// + /// All PTE bytes we read here are what the kernel previously wrote into guest memory; + /// the host MMU just interprets them to enforce translations. fn translate(&self, va: VirtualAddress, kind: MemoryAccessKind) -> Option { + let root_base = self.root_base()?; let vpn1 = va.vpn1() as usize; let vpn0 = va.vpn0() as usize; let offset = va.offset() as usize; - let roots = self.root_tables.borrow(); - let root = roots - .get(self.current_root.get()) - .unwrap_or_else(|| panic!("invalid root index {}", self.current_root.get())); - let l2_idx = root.get(vpn1).and_then(|pte| pte.next_l2)?; - let l2s = self.l2_tables.borrow(); - let l2 = l2s.get(l2_idx)?; - let leaf = l2.get(vpn0)?; - if !leaf.valid || !leaf.is_leaf() { + + let root_pte_addr = root_base + vpn1 * core::mem::size_of::(); + let root_pte = self.read_pte(root_pte_addr)?; + if root_pte & PTE_V == 0 { return None; } - // Basic permission check: align MemoryAccessKind to R/W. + + // We only support two-level translation; reject L1 leaf/superpages. + if root_pte & (PTE_R | PTE_W | PTE_X) != 0 { + return None; + } + + let l2_ppn = (root_pte >> 10) as usize; + let l2_base = l2_ppn + .checked_mul(self.page_size) + .expect("l2 base overflow"); + let l2_pte_addr = l2_base + vpn0 * core::mem::size_of::(); + let l2_pte = self.read_pte(l2_pte_addr)?; + if l2_pte & PTE_V == 0 { + return None; + } + let allowed = match kind { - MemoryAccessKind::Load | MemoryAccessKind::ReservationLoad => leaf.read || leaf.exec, - MemoryAccessKind::Store - | MemoryAccessKind::Atomic - | MemoryAccessKind::ReservationStore => leaf.write, + MemoryAccessKind::Load | MemoryAccessKind::ReservationLoad => l2_pte & (PTE_R | PTE_X) != 0, + MemoryAccessKind::Store | MemoryAccessKind::Atomic | MemoryAccessKind::ReservationStore => l2_pte & PTE_W != 0, }; if !allowed { return None; } - let pa = (leaf.ppn << PAGE_SHIFT) + offset; - Some(pa) + + let leaf_ppn = (l2_pte >> 10) as usize; + leaf_ppn + .checked_mul(self.page_size) + .and_then(|base| base.checked_add(offset)) } fn meter_access( @@ -182,6 +271,18 @@ impl Memory { ) } + fn next_heap_for_root(&self) -> VirtualAddress { + let key = self.satp.get(); + let mut heaps = self.per_root_heap.borrow_mut(); + *heaps.entry(key).or_insert_with(|| VirtualAddress(0)) + } + + fn set_next_heap_for_root(&self, next: VirtualAddress) { + let key = self.satp.get(); + let mut heaps = self.per_root_heap.borrow_mut(); + heaps.insert(key, next); + } + /// Copy a slice into physical backing, honoring translation and page boundaries. fn copy_into_backing(&self, start: VirtualAddress, data: &[u8], kind: MemoryAccessKind) { let mut remaining = data.len(); @@ -211,6 +312,29 @@ impl Memory { pub fn write_bytes(&self, start: VirtualAddress, data: &[u8]) { self.copy_into_backing(start, data, MemoryAccessKind::Store); } + + /// Allocate space on the per-root heap, map it writable, and copy data. + pub fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { + let mut addr = self.next_heap_for_root().as_u32(); + let align = 8; + addr = (addr + (align - 1)) & !(align - 1); + let end = addr + data.len() as u32; + let start_va = VirtualAddress(addr); + self.map_range(start_va, data.len(), Perms::rw_kernel()); + + self.copy_into_backing(start_va, data, MemoryAccessKind::Store); + let end_va = VirtualAddress(end); + self.set_next_heap_for_root(end_va); + start_va + } + + pub fn next_heap(&self) -> VirtualAddress { + self.next_heap_for_root() + } + + pub fn set_next_heap(&self, next: VirtualAddress) { + self.set_next_heap_for_root(next); + } } impl Mmu for Memory { @@ -222,16 +346,8 @@ impl Mmu for Memory { Memory::map_range(self, start, len, perms); } - fn set_root(&self, root: usize) { - let roots = self.root_tables.borrow(); - if root >= roots.len() { - panic!("set_root: invalid root index {}", root); - } - self.current_root.set(root); - } - fn current_root(&self) -> usize { - self.current_root.get() + self.root_ppn() } fn mem_slice( @@ -375,37 +491,35 @@ impl Mmu for Memory { )) } - fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { - let mut addr = self.next_heap.get().as_u32(); - let align = 8; - addr = (addr + (align - 1)) & !(align - 1); - let end = addr + data.len() as u32; - let start_va = VirtualAddress(addr); - self.map_range(start_va, data.len(), Perms::rw_kernel()); + fn size(&self) -> usize { + self.total_size() + } - self.copy_into_backing(start_va, data, MemoryAccessKind::Store); - let end_va = VirtualAddress(end); - self.next_heap.set(end_va); - start_va + fn offset(&self, addr: VirtualAddress) -> usize { + addr.as_usize() } - fn stack_top(&self) -> VirtualAddress { - VirtualAddress(self.total_size() as u32) + fn set_satp(&self, satp: u32) { + self.satp.set(satp & SATP_PPN_MASK); } - fn size(&self) -> usize { - self.total_size() + fn satp(&self) -> u32 { + self.satp.get() } - fn offset(&self, addr: VirtualAddress) -> usize { - addr.as_usize() + fn stack_top(&self) -> VirtualAddress { + VirtualAddress(self.total_size() as u32) + } + + fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { + Memory::alloc_on_heap(self, data) } fn next_heap(&self) -> VirtualAddress { - self.next_heap.get() + self.next_heap_for_root() } fn set_next_heap(&self, next: VirtualAddress) { - self.next_heap.set(next); + self.set_next_heap_for_root(next); } } diff --git a/crates/kernel/src/config.rs b/crates/kernel/src/config.rs index 6904ce6..e39b61b 100644 --- a/crates/kernel/src/config.rs +++ b/crates/kernel/src/config.rs @@ -1,5 +1,3 @@ -#![no_std] - pub struct Config; impl Config { diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 83176ab..7acc9e9 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -17,7 +17,7 @@ impl Global { /// # Safety /// Callers must ensure exclusive access or otherwise serialize mutations. pub unsafe fn get_mut(&self) -> &mut T { - &mut *self.inner.get() + unsafe { &mut *self.inner.get() } } } diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index 1afd784..9e930b6 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -1,17 +1,8 @@ #![allow(dead_code)] -use crate::{AddressSpace, Config, Task}; +use crate::{AddressSpace, Config, Task, mmu}; use program::logf; -// Linux-style protection and mapping flags (subset). -const PROT_READ: u32 = 0x1; -const PROT_WRITE: u32 = 0x2; -const PROT_EXEC: u32 = 0x4; - -const MAP_PRIVATE: u32 = 0x02; -const MAP_ANON: u32 = 0x20; -const MAP_FIXED: u32 = 0x10; - const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap @@ -27,23 +18,6 @@ pub const PROGRAM_WINDOW_BYTES: usize = align_up( PAGE_SIZE, ); -fn syscall_mmap(addr: u32, len: usize, prot: u32, flags: u32) -> u32 { - let ret: u32; - unsafe { - core::arch::asm!( - "ecall", - in("a7") 222u32, // __NR_mmap - inlateout("a0") addr => ret, - in("a1") len as u32, - in("a2") prot, - in("a3") flags, - in("a4") 0u32, // fd - in("a5") 0u32, // offset - ); - } - ret -} - /// Create a new task for a program and map its virtual address window via syscalls. /// /// This sets up: @@ -53,21 +27,12 @@ fn syscall_mmap(addr: u32, len: usize, prot: u32, flags: u32) -> u32 { /// The caller is responsible for copying program bytes into the mapped window /// and initializing the user trapframe (PC/SP/args) before running. pub fn launch_program(asid: u16, kstack_top: u32) -> Option { - let prot = PROT_READ | PROT_WRITE | PROT_EXEC; - let flags = MAP_PRIVATE | MAP_ANON | MAP_FIXED; - let addr = syscall_mmap(PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, prot, flags); - if addr != PROGRAM_VA_BASE { - logf!("launch_program: mmap failed"); + let perms = mmu::PagePerms::user_rwx(); + if !mmu::map_user_range(PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { + logf!("launch_program: mapping failed"); return None; } // Root/asid tracking is minimal; real satp handling is TBD. - let task = Task::new(AddressSpace::new(0, asid), kstack_top); - logf!( - "launch_program: asid=%d base=0x%x size=%d", - asid as u32, - PROGRAM_VA_BASE, - PROGRAM_WINDOW_BYTES as u32 - ); - Some(task) + Some(Task::new(AddressSpace::new(0, asid), kstack_top)) } diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 47d3249..c8d4f40 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -3,7 +3,9 @@ pub mod config; pub use config::Config; pub use types::boot::BootInfo; +pub mod global; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; pub mod launch; pub use launch::{launch_program, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; +pub mod mmu; diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 1536590..7053eb9 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -7,7 +7,7 @@ use alloc::{format, vec, vec::Vec}; use core::mem::forget; use core::ptr; use core::slice; -use kernel::{BootInfo, Config, Task, launch_program, PROGRAM_WINDOW_BYTES}; +use kernel::{BootInfo, Config, Task, launch_program, mmu, PROGRAM_WINDOW_BYTES}; use program::{log, logf}; use state::State; use types::transaction::{Transaction, TransactionBundle, TransactionType}; @@ -22,6 +22,7 @@ const KERNEL_TASK_IDX: usize = 0; static TASKS: Global>> = Global::new(None); static STATE: Global> = Global::new(None); static BOOT_INFO: Global> = Global::new(None); +static PAGE_ALLOC_INIT: Global = Global::new(false); /// Kernel entrypoint. Receives: /// - `bundle_ptr`/`bundle_len`: encoded `TransactionBundle` prepared by the bootloader. @@ -59,6 +60,12 @@ pub extern "C" fn _start( unsafe { *BOOT_INFO.get_mut() = Some(*info); } + unsafe { + if !*PAGE_ALLOC_INIT.get_mut() { + mmu::init(info); + *PAGE_ALLOC_INIT.get_mut() = true; + } + } let task = Task::kernel(info.root_ppn, info.kstack_top); unsafe { let tasks_slot = TASKS.get_mut(); diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/mmu.rs new file mode 100644 index 0000000..e70b6ba --- /dev/null +++ b/crates/kernel/src/mmu.rs @@ -0,0 +1,170 @@ +use crate::global::Global; +use crate::BootInfo; + +const PAGE_SIZE: usize = 4096; +const VPN_MASK: u32 = 0x3ff; +const PTE_V: u32 = 1 << 0; +const PTE_R: u32 = 1 << 1; +const PTE_W: u32 = 1 << 2; +const PTE_X: u32 = 1 << 3; +const PTE_U: u32 = 1 << 4; + +#[derive(Clone, Copy)] +pub struct PagePerms { + pub read: bool, + pub write: bool, + pub exec: bool, + pub user: bool, +} + +impl PagePerms { + pub const fn user_rwx() -> Self { + Self { + read: true, + write: true, + exec: true, + user: true, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct PageAllocator { + next_ppn: u32, + limit_ppn: u32, +} + +impl PageAllocator { + /// Create a bump-frame allocator over [start_ppn, limit_ppn). + pub const fn new(start_ppn: u32, limit_ppn: u32) -> Self { + Self { + next_ppn: start_ppn, + limit_ppn, + } + } + + /// Allocate the next free physical page number, or None if exhausted. + pub fn alloc(&mut self) -> Option { + if self.next_ppn >= self.limit_ppn { + return None; + } + let ppn = self.next_ppn; + self.next_ppn += 1; + Some(ppn) + } + + /// Zero a 4 KiB page in guest physical memory. + fn zero_page(ppn: u32) { + let base = (ppn as usize) * PAGE_SIZE; + unsafe { + core::ptr::write_bytes(base as *mut u8, 0, PAGE_SIZE); + } + } +} + +static ROOT_PPN: Global = Global::new(0); +static PAGE_ALLOC: Global> = Global::new(None); + +/// Initialize the kernel MMU allocator state from bootloader handoff. +pub fn init(boot_info: &BootInfo) { + unsafe { + *ROOT_PPN.get_mut() = boot_info.root_ppn; + let limit_ppn = (boot_info.memory_size as usize / PAGE_SIZE) as u32; + *PAGE_ALLOC.get_mut() = Some(PageAllocator::new(boot_info.next_free_ppn, limit_ppn)); + } +} + +/// Map a user-visible virtual range with the provided permissions into the current root. +pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { + let root = unsafe { *ROOT_PPN.get_mut() }; + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => map_range(root, va_start, len, perms, alloc), + None => false, + } +} + +fn map_range(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms, alloc: &mut PageAllocator) -> bool { + if len == 0 { + return true; + } + let start = align_down(va_start as usize, PAGE_SIZE) as u32; + let end = (va_start as usize).saturating_add(len); + let end_aligned = align_up(end, PAGE_SIZE) as u32; + + let mut va = start; + while va < end_aligned { + if !map_page(root_ppn, va, perms, alloc) { + return false; + } + va = va.wrapping_add(PAGE_SIZE as u32); + } + true +} + +/// Map a single 4 KiB page at `va` into the two-level Sv32 page tables rooted at `root_ppn`. +/// Allocates an L2 table if the L1 entry is absent; refuses superpages; fills in a leaf PTE +/// with the requested permissions after zeroing the backing frame. +fn map_page(root_ppn: u32, va: u32, perms: PagePerms, alloc: &mut PageAllocator) -> bool { + let vpn1 = (va >> 22) & VPN_MASK; + let vpn0 = (va >> 12) & VPN_MASK; + + // L1 lookup + let root_base = (root_ppn as usize) * PAGE_SIZE; + let l1_entry_addr = root_base + vpn1 as usize * core::mem::size_of::(); + let mut l1_pte = unsafe { (l1_entry_addr as *const u32).read_volatile() }; + + if l1_pte & PTE_V == 0 { + let l2 = match alloc.alloc() { + Some(ppn) => ppn, + None => return false, + }; + PageAllocator::zero_page(l2); + l1_pte = ((l2 as u32) << 10) | PTE_V; + unsafe { (l1_entry_addr as *mut u32).write_volatile(l1_pte) }; + } else if l1_pte & (PTE_R | PTE_W | PTE_X) != 0 { + // Superpages not supported. + return false; + } + + let l2_ppn = l1_pte >> 10; + let l2_base = (l2_ppn as usize) * PAGE_SIZE; + let l2_entry_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + + let existing = unsafe { (l2_entry_addr as *const u32).read_volatile() }; + if existing & PTE_V != 0 { + // Already mapped. + return true; + } + + let frame = match alloc.alloc() { + Some(ppn) => ppn, + None => return false, + }; + PageAllocator::zero_page(frame); + + let mut flags = PTE_V; + if perms.read { + flags |= PTE_R; + } + if perms.write { + flags |= PTE_W; + } + if perms.exec { + flags |= PTE_X; + } + if perms.user { + flags |= PTE_U; + } + let leaf = ((frame as u32) << 10) | flags; + unsafe { (l2_entry_addr as *mut u32).write_volatile(leaf) }; + true +} + +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +const fn align_down(val: usize, align: usize) -> usize { + val & !(align - 1) +} diff --git a/crates/types/src/boot.rs b/crates/types/src/boot.rs index 3cb8d6d..b55b56a 100644 --- a/crates/types/src/boot.rs +++ b/crates/types/src/boot.rs @@ -16,14 +16,17 @@ pub struct BootInfo { pub kstack_top: u32, /// Total physical memory size in bytes. pub memory_size: u32, + /// First free physical page number after bootloader allocations. + pub next_free_ppn: u32, } impl BootInfo { - pub const fn new(root_ppn: u32, kstack_top: u32, memory_size: u32) -> Self { + pub const fn new(root_ppn: u32, kstack_top: u32, memory_size: u32, next_free_ppn: u32) -> Self { Self { root_ppn, kstack_top, memory_size, + next_free_ppn, } } } diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 648b3e8..0ba0562 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -11,6 +11,8 @@ use std::rc::Rc; #[path = "exe.rs"] mod exec; +pub const CSR_SATP: u16 = 0x180; + /// Represents the Central Processing Unit (CPU) of our RISC-V virtual machine. /// /// EDUCATIONAL PURPOSE: This struct models the core components of a real CPU: diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index d9b63a3..afa56d2 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,4 +1,4 @@ -use super::{Instruction, MemoryAccessKind, Memory, CPU}; +use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP}; use crate::memory::VirtualAddress; use crate::host_interface::HostInterface; use crate::instruction::CsrOp; @@ -845,7 +845,11 @@ impl CPU { } } - if src != 0 || matches!(op, CsrOp::Csrrw) { + let will_write = src != 0 || matches!(op, CsrOp::Csrrw); + if will_write { + if csr == CSR_SATP { + memory.set_satp(new_val); + } if !self.write_csr(csr, new_val) { return false; } diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory.rs index 57d2419..3b8030c 100644 --- a/crates/vm/src/memory.rs +++ b/crates/vm/src/memory.rs @@ -104,14 +104,16 @@ pub trait Mmu: std::fmt::Debug { fn load_byte(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_halfword(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_word(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; - /// Map a virtual range with the provided permissions. fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms); - /// Set the current page-table root (index/identifier) used for translation. - fn set_root(&self, root: usize); /// Get the current page-table root (index/identifier). fn current_root(&self) -> usize; - fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress; + /// Read the current satp value. + fn satp(&self) -> u32; + /// Set satp (PPN field is used for the root in this emulator). + fn set_satp(&self, satp: u32); + /// Top of the stack for this memory layout. fn stack_top(&self) -> VirtualAddress; + fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress; fn size(&self) -> usize; fn offset(&self, addr: VirtualAddress) -> usize; fn next_heap(&self) -> VirtualAddress; diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 5db5d68..5446052 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,6 +1,6 @@ -use crate::cpu::CPU; +use crate::cpu::{CPU, CSR_SATP}; use crate::host_interface::HostInterface; -use crate::memory::{Memory, VirtualAddress}; +use crate::memory::Memory; use crate::metering::Metering; use crate::registers::Register; use crate::sys_call::SyscallHandler; @@ -50,6 +50,7 @@ impl VM { ) -> Self { let mut cpu = CPU::new(syscall_handler); cpu.regs[Register::Sp as usize] = memory.stack_top().as_u32(); + cpu.csrs.insert(CSR_SATP, memory.satp()); Self { cpu, memory, @@ -61,58 +62,6 @@ impl VM { pub fn set_metering(&mut self, metering: Box) { self.cpu.set_metering(metering); } - - /// Allocates memory on the heap and writes data to it. - /// - /// EDUCATIONAL PURPOSE: This demonstrates dynamic memory allocation in a VM. - /// Programs need to allocate memory for variables, arrays, and other data - /// structures at runtime. - /// - /// HEAP MANAGEMENT: The VM maintains a heap pointer that moves forward - /// as memory is allocated. This is a simple but effective allocation strategy. - /// - /// RETURN VALUE: Returns the address where the data was written - pub fn alloc_and_write(&mut self, data: &[u8]) -> u32 { - self.memory.alloc_on_heap(data).as_u32() - } - - /// Sets a register to point to data in memory. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how to pass data to programs - /// running in the VM. Instead of copying data into registers (which are - /// limited in size), we store the data in memory and pass the address. - /// - /// PARAMETER PASSING: This is how we pass strings, arrays, and other - /// large data structures to programs. The register contains a pointer - /// to the actual data in memory. - /// - /// DEBUG OUTPUT: The function prints information about what it's doing, - /// which is helpful for understanding VM behavior during development. - pub fn set_reg_to_data(&mut self, reg: Register, data: &[u8]) -> u32 { - // EDUCATIONAL: Allocate memory and write the data - let addr = self.alloc_and_write(data); - - // EDUCATIONAL: Set the register to point to the data - self.cpu.regs[reg as usize] = addr; - - // EDUCATIONAL: Debug output to help understand what's happening - println!( - "📥 set reg x{} to addr 0x{:08x} (len = {})", - reg as u32, - addr, - data.len() - ); - - addr - } - - /// Sets a register to a 32-bit value. - /// - /// EDUCATIONAL PURPOSE: This is used for passing small values (like - /// integers) directly to programs. For larger data, use set_reg_to_data. - /// - /// USAGE: Typically used for passing function parameters, flags, or - /// other small values that fit in a single register. pub fn set_reg_u32(&mut self, reg: Register, data: u32) { self.cpu.regs[reg as usize] = data; } From 387efcb514ab16f9f7a9159b468b71db439c2039 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 23 Dec 2025 13:25:31 +0200 Subject: [PATCH 22/70] Refactor kernel globals and init setup --- crates/bootloader/src/bootloader.rs | 11 +- crates/bootloader/src/memory/memory.rs | 165 ++++++++-------- crates/kernel/src/global.rs | 16 ++ crates/kernel/src/init.rs | 62 ++++++ crates/kernel/src/launch.rs | 88 ++++++++- crates/kernel/src/main.rs | 66 +------ crates/kernel/src/mmu.rs | 229 ++++++++++++---------- crates/types/src/lib.rs | 3 + crates/types/src/mmu.rs | 250 +++++++++++++++++++++++++ 9 files changed, 637 insertions(+), 253 deletions(-) create mode 100644 crates/kernel/src/init.rs create mode 100644 crates/types/src/mmu.rs diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 57e3542..88bbd16 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -6,7 +6,7 @@ use std::vec::Vec; use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; -use types::{boot::BootInfo, transaction::TransactionBundle}; +use types::{boot::BootInfo, transaction::TransactionBundle, SV32_DIRECT_MAP_BASE}; use crate::DefaultSyscallHandler; use crate::memory::{Memory, Perms}; @@ -96,6 +96,15 @@ impl Bootloader { .saturating_sub(KERNEL_STACK_BYTES); self.memory .map_range(VirtualAddress(stack_base as u32), KERNEL_STACK_BYTES, Perms::rw_kernel()); + // Map a direct window over all physical memory so the kernel can touch + // page tables after paging is enabled. + let mapped = self.memory.map_physical_range( + VirtualAddress(SV32_DIRECT_MAP_BASE), + 0, + self.memory.size(), + Perms::rw_kernel(), + ); + assert!(mapped, "failed to map kernel direct physical window"); (entry_point, self.memory.clone() as MmuRef) } diff --git a/crates/bootloader/src/memory/memory.rs b/crates/bootloader/src/memory/memory.rs index f484edd..fef734a 100644 --- a/crates/bootloader/src/memory/memory.rs +++ b/crates/bootloader/src/memory/memory.rs @@ -2,19 +2,13 @@ use std::cell::{Cell, Ref, RefCell}; use std::collections::HashMap; use std::rc::Rc; +use types::{ + Sv32PagePerms, Sv32PageTable, SV32_PTE_R, SV32_PTE_V, SV32_PTE_W, SV32_PTE_X, + SV32_SATP_PPN_MASK, map_allocating, map_to_physical, +}; use vm::memory::{Mmu, Perms, VirtualAddress}; use vm::metering::{MeterResult, Metering, MemoryAccessKind}; -// Minimal Sv32 bit layout for guest-visible PTEs. -const PTE_V: u32 = 1 << 0; -const PTE_R: u32 = 1 << 1; -const PTE_W: u32 = 1 << 2; -const PTE_X: u32 = 1 << 3; -const PTE_U: u32 = 1 << 4; - -// Sv32 satp PPN mask (bits 21:0). Mode bits are ignored in this emulator. -const SATP_PPN_MASK: u32 = 0x003f_ffff; - /// Software Sv32 MMU backed by a contiguous physical buffer. /// /// Design at a glance: @@ -48,6 +42,15 @@ pub struct Memory { next_free_frame: Cell, } +fn perms_to_sv32(perms: Perms) -> Sv32PagePerms { + Sv32PagePerms { + read: perms.read, + write: perms.write, + exec: perms.exec, + user: perms.user, + } +} + impl Memory { pub fn new(total_size_bytes: usize, page_size: usize) -> Self { assert!(page_size != 0, "page_size must be > 0"); @@ -57,16 +60,19 @@ impl Memory { let total = total_pages .checked_mul(page_size) .expect("physical memory size overflow"); - // Reserve frame 0 for the initial root page table. - let root_ppn: usize = 0; - Self { + // Reserve frame 0; place the initial root page table at frame 1. + let root_ppn: usize = 1; + let mem = Self { page_size, total_pages, backing: Rc::new(RefCell::new(vec![0u8; total])), satp: Cell::new(root_ppn as u32), per_root_heap: RefCell::new(HashMap::new()), next_free_frame: Cell::new(root_ppn + 1), - } + }; + // Zero the root page table frame so we can immediately populate it. + mem.zero_frame(root_ppn); + mem } fn total_size(&self) -> usize { @@ -74,7 +80,7 @@ impl Memory { } fn root_ppn(&self) -> usize { - (self.satp.get() & SATP_PPN_MASK) as usize + (self.satp.get() & SV32_SATP_PPN_MASK) as usize } fn root_base(&self) -> Option { @@ -131,80 +137,29 @@ impl Memory { backing[phys_addr..end].copy_from_slice(&val.to_le_bytes()); } - /// Map a single 4 KiB page at `va` with the given permissions, allocating tables/frames as needed. - fn map_page(&self, va: VirtualAddress, perms: Perms) { - let root_base = self.root_base().expect("invalid root page table base"); - let vpn1 = va.vpn1() as usize; - let vpn0 = va.vpn0() as usize; - - // Ensure L2 table exists. - let root_pte_addr = root_base + vpn1 * core::mem::size_of::(); - let root_pte = self.read_pte(root_pte_addr).unwrap_or(0); - let root_is_valid = root_pte & PTE_V != 0; - let root_has_perms = root_pte & (PTE_R | PTE_W | PTE_X) != 0; - - if root_is_valid && root_has_perms { - panic!("superpages not supported"); - } - - let l2_ppn = if root_is_valid { - (root_pte >> 10) as usize - } else { - let l2_frame = self - .allocate_frame() - .expect("out of physical frames while mapping L2"); - self.zero_frame(l2_frame); - let new_pte = ((l2_frame as u32) << 10) | PTE_V; - self.write_pte(root_pte_addr, new_pte); - l2_frame - }; - - let l2_base = l2_ppn - .checked_mul(self.page_size) - .expect("l2 base overflow"); - let leaf_addr = l2_base + vpn0 * core::mem::size_of::(); - - if let Some(existing) = self.read_pte(leaf_addr) { - if existing & PTE_V != 0 { - return; - } - } - - let frame = self - .allocate_frame() - .expect("out of physical frames while mapping leaf"); - self.zero_frame(frame); - - let mut flags = PTE_V; - if perms.read { - flags |= PTE_R; - } - if perms.write { - flags |= PTE_W; - } - if perms.exec { - flags |= PTE_X; - } - if perms.user { - flags |= PTE_U; - } - let leaf_pte = ((frame as u32) << 10) | flags; - self.write_pte(leaf_addr, leaf_pte); - } - /// Map a contiguous virtual range page-by-page with the given permissions. pub fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { - if len == 0 { - return; - } - let start_addr = start.align_down().as_usize(); - let end_addr = start.as_usize().saturating_add(len); + let root = self.root_ppn() as u32; + let ok = map_allocating(self, root, start.as_u32(), len, perms_to_sv32(perms)); + assert!(ok, "map_range failed"); + } - let mut page_start = start_addr; - while page_start < end_addr { - self.map_page(VirtualAddress(page_start as u32), perms); - page_start = page_start.saturating_add(self.page_size); - } + /// Map a virtual range to a specific physical range without allocating new leaf frames. + pub fn map_physical_range( + &self, + va_start: VirtualAddress, + phys_start: u32, + len: usize, + perms: Perms, + ) -> bool { + map_to_physical( + self, + self.root_ppn() as u32, + va_start.as_u32(), + phys_start, + len, + perms_to_sv32(perms), + ) } /// Translate a virtual address to a physical offset into `backing`, checking permissions. @@ -226,12 +181,12 @@ impl Memory { let root_pte_addr = root_base + vpn1 * core::mem::size_of::(); let root_pte = self.read_pte(root_pte_addr)?; - if root_pte & PTE_V == 0 { + if root_pte & SV32_PTE_V == 0 { return None; } // We only support two-level translation; reject L1 leaf/superpages. - if root_pte & (PTE_R | PTE_W | PTE_X) != 0 { + if root_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { return None; } @@ -241,13 +196,17 @@ impl Memory { .expect("l2 base overflow"); let l2_pte_addr = l2_base + vpn0 * core::mem::size_of::(); let l2_pte = self.read_pte(l2_pte_addr)?; - if l2_pte & PTE_V == 0 { + if l2_pte & SV32_PTE_V == 0 { return None; } let allowed = match kind { - MemoryAccessKind::Load | MemoryAccessKind::ReservationLoad => l2_pte & (PTE_R | PTE_X) != 0, - MemoryAccessKind::Store | MemoryAccessKind::Atomic | MemoryAccessKind::ReservationStore => l2_pte & PTE_W != 0, + MemoryAccessKind::Load | MemoryAccessKind::ReservationLoad => { + l2_pte & (SV32_PTE_R | SV32_PTE_X) != 0 + } + MemoryAccessKind::Store | MemoryAccessKind::Atomic | MemoryAccessKind::ReservationStore => { + l2_pte & SV32_PTE_W != 0 + } }; if !allowed { return None; @@ -337,6 +296,28 @@ impl Memory { } } +impl Sv32PageTable for Memory { + fn page_size(&self) -> usize { + self.page_size + } + + fn read_pte(&self, phys_addr: usize) -> Option { + self.read_pte(phys_addr) + } + + fn write_pte(&self, phys_addr: usize, val: u32) { + self.write_pte(phys_addr, val); + } + + fn alloc_frame(&self) -> Option { + self.allocate_frame().map(|ppn| ppn as u32) + } + + fn zero_frame(&self, ppn: u32) { + self.zero_frame(ppn as usize); + } +} + impl Mmu for Memory { fn mem(&self) -> Ref> { self.backing.borrow() @@ -500,7 +481,7 @@ impl Mmu for Memory { } fn set_satp(&self, satp: u32) { - self.satp.set(satp & SATP_PPN_MASK); + self.satp.set(satp & SV32_SATP_PPN_MASK); } fn satp(&self) -> u32 { diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 7acc9e9..0c9e40d 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -1,4 +1,11 @@ +extern crate alloc; + +use alloc::vec::Vec; use core::cell::UnsafeCell; +use state::State; + +use crate::{BootInfo, Task}; +use crate::mmu::PageAllocator; /// Minimal wrapper to store non-`Sync` types in statics. /// @@ -22,3 +29,12 @@ impl Global { } unsafe impl Sync for Global {} + +#[allow(dead_code)] +pub static TASKS: Global>> = Global::new(None); +pub static STATE: Global> = Global::new(None); +pub static BOOT_INFO: Global> = Global::new(None); +pub static PAGE_ALLOC_INIT: Global = Global::new(false); +pub static NEXT_ASID: Global = Global::new(1); +pub static ROOT_PPN: Global = Global::new(0); +pub static PAGE_ALLOC: Global> = Global::new(None); diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs new file mode 100644 index 0000000..6c42658 --- /dev/null +++ b/crates/kernel/src/init.rs @@ -0,0 +1,62 @@ +use alloc::vec; +use core::slice; + +use program::{log, logf}; +use state::State; + +use kernel::global::{BOOT_INFO, PAGE_ALLOC_INIT, STATE, TASKS}; +use kernel::{mmu, BootInfo, Task}; + +/// Initialize kernel state from the bootloader handoff and optional state blob. +pub fn init_kernel(state_ptr: *const u8, state_len: usize, boot_info_ptr: *const BootInfo) { + init_state(state_ptr, state_len); + init_boot_info(boot_info_ptr); +} + +fn init_state(state_ptr: *const u8, state_len: usize) { + unsafe { + let state_slot = STATE.get_mut(); + if !state_ptr.is_null() && state_len > 0 { + let bytes = slice::from_raw_parts(state_ptr, state_len); + *state_slot = State::decode(bytes).or_else(|| { + log!("state decode failed; starting empty state"); + Some(State::new()) + }); + if state_slot.is_some() { + logf!("state initialized (len=%d)", state_len as u32); + } + } else { + *state_slot = Some(State::new()); + } + } +} + +fn init_boot_info(boot_info_ptr: *const BootInfo) { + if let Some(info) = unsafe { boot_info_ptr.as_ref() } { + unsafe { + *BOOT_INFO.get_mut() = Some(*info); + } + unsafe { + if !*PAGE_ALLOC_INIT.get_mut() { + mmu::init(info); + *PAGE_ALLOC_INIT.get_mut() = true; + } + } + let task = Task::kernel(info.root_ppn, info.kstack_top); + unsafe { + let tasks_slot = TASKS.get_mut(); + match tasks_slot { + Some(tasks) => tasks.push(task), + None => *tasks_slot = Some(vec![task]), + } + } + logf!( + "boot_info: root_ppn=0x%x kstack_top=0x%x mem_size=%d", + info.root_ppn, + info.kstack_top, + info.memory_size + ); + } else { + log!("boot_info missing; kernel task not initialized"); + } +} diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index 9e930b6..9f22346 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -1,13 +1,15 @@ #![allow(dead_code)] use crate::{AddressSpace, Config, Task, mmu}; +use crate::global::NEXT_ASID; +use program::log; use program::logf; +use types::{address::Address, ADDRESS_LEN}; const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap pub const PROGRAM_VA_BASE: u32 = 0x0; - const fn align_up(val: usize, align: usize) -> usize { (val + (align - 1)) & !(align - 1) } @@ -18,6 +20,16 @@ pub const PROGRAM_WINDOW_BYTES: usize = align_up( PAGE_SIZE, ); +const REG_SP: usize = 2; +const REG_A0: usize = 10; +const REG_A1: usize = 11; +const REG_A2: usize = 12; +const REG_A3: usize = 13; + +const TO_PTR_ADDR: u32 = 0x120; +const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; +const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; + /// Create a new task for a program and map its virtual address window via syscalls. /// /// This sets up: @@ -26,13 +38,77 @@ pub const PROGRAM_WINDOW_BYTES: usize = align_up( /// /// The caller is responsible for copying program bytes into the mapped window /// and initializing the user trapframe (PC/SP/args) before running. -pub fn launch_program(asid: u16, kstack_top: u32) -> Option { +pub fn launch_program( + kstack_top: u32, + to: &Address, + from: &Address, + code: &[u8], + input: &[u8], +) -> Option { + if input.len() > Config::MAX_INPUT_LEN { + log!("launch_program: input too large"); + return None; + } + + let asid = alloc_asid(); + let root_ppn = match mmu::alloc_root() { + Some(ppn) => ppn, + None => { + logf!("launch_program: no free root PPN available"); + return None; + } + }; + let window_end = PROGRAM_VA_BASE.wrapping_add(PROGRAM_WINDOW_BYTES as u32); + logf!( + "launch_program: asid=%d root=0x%x map=[0x%x,0x%x)", + asid as u32, + root_ppn, + PROGRAM_VA_BASE, + window_end + ); let perms = mmu::PagePerms::user_rwx(); - if !mmu::map_user_range(PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { - logf!("launch_program: mapping failed"); + if !mmu::map_user_range_for_root(root_ppn, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { + logf!("launch_program: mapping failed (root=0x%x)", root_ppn); return None; } - // Root/asid tracking is minimal; real satp handling is TBD. - Some(Task::new(AddressSpace::new(0, asid), kstack_top)) + // Copy code and arguments into user memory. + if !mmu::copy_into_user(root_ppn, Config::PROGRAM_START_ADDR, code) { + logf!("launch_program: failed to copy code into root=0x%x", root_ppn); + return None; + } + if !mmu::copy_into_user(root_ppn, TO_PTR_ADDR, &to.0) { + logf!("launch_program: failed to copy 'to' address into root=0x%x", root_ppn); + return None; + } + if !mmu::copy_into_user(root_ppn, FROM_PTR_ADDR, &from.0) { + logf!("launch_program: failed to copy 'from' address into root=0x%x", root_ppn); + return None; + } + if !mmu::copy_into_user(root_ppn, INPUT_BASE_ADDR, input) { + logf!("launch_program: failed to copy input into root=0x%x", root_ppn); + return None; + } + + let mut task = Task::new(AddressSpace::new(root_ppn, asid), kstack_top); + // Set up initial trapframe. + let stack_top = PROGRAM_VA_BASE + .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); + task.tf.pc = Config::PROGRAM_START_ADDR; + task.tf.regs[REG_SP] = stack_top; + task.tf.regs[REG_A0] = TO_PTR_ADDR; + task.tf.regs[REG_A1] = FROM_PTR_ADDR; + task.tf.regs[REG_A2] = INPUT_BASE_ADDR; + task.tf.regs[REG_A3] = input.len() as u32; + + Some(task) +} + +fn alloc_asid() -> u16 { + unsafe { + let counter = NEXT_ASID.get_mut(); + let asid = if *counter == 0 { 1 } else { *counter }; + *counter = asid.wrapping_add(1); + asid + } } diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 7053eb9..307e921 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -3,27 +3,21 @@ extern crate alloc; -use alloc::{format, vec, vec::Vec}; +use alloc::{format, vec}; use core::mem::forget; -use core::ptr; use core::slice; -use kernel::{BootInfo, Config, Task, launch_program, mmu, PROGRAM_WINDOW_BYTES}; +use kernel::{BootInfo, Config, launch_program, PROGRAM_WINDOW_BYTES}; use program::{log, logf}; use state::State; use types::transaction::{Transaction, TransactionBundle, TransactionType}; -mod global; -use crate::global::Global; +mod init; +use kernel::global::{BOOT_INFO, STATE, TASKS}; +use crate::init::init_kernel; #[allow(dead_code)] const KERNEL_TASK_IDX: usize = 0; -#[allow(dead_code)] -static TASKS: Global>> = Global::new(None); -static STATE: Global> = Global::new(None); -static BOOT_INFO: Global> = Global::new(None); -static PAGE_ALLOC_INIT: Global = Global::new(false); - /// Kernel entrypoint. Receives: /// - `bundle_ptr`/`bundle_len`: encoded `TransactionBundle` prepared by the bootloader. /// - `state_ptr`/`state_len`: optional state blob (currently unused). @@ -39,50 +33,7 @@ pub extern "C" fn _start( log!("kernel boot"); logf!("bundle_len=%d", bundle_len as u32); - // Initialize state from provided blob if present. - unsafe { - let state_slot = STATE.get_mut(); - if !state_ptr.is_null() && state_len > 0 { - let bytes = slice::from_raw_parts(state_ptr, state_len); - *state_slot = State::decode(bytes).or_else(|| { - log!("state decode failed; starting empty state"); - Some(State::new()) - }); - if state_slot.is_some() { - logf!("state initialized (len=%d)", state_len as u32); - } - } else { - *state_slot = Some(State::new()); - } - } - - if let Some(info) = unsafe { boot_info_ptr.as_ref() } { - unsafe { - *BOOT_INFO.get_mut() = Some(*info); - } - unsafe { - if !*PAGE_ALLOC_INIT.get_mut() { - mmu::init(info); - *PAGE_ALLOC_INIT.get_mut() = true; - } - } - let task = Task::kernel(info.root_ppn, info.kstack_top); - unsafe { - let tasks_slot = TASKS.get_mut(); - match tasks_slot { - Some(tasks) => tasks.push(task), - None => *tasks_slot = Some(vec![task]), - } - } - logf!( - "boot_info: root_ppn=0x%x kstack_top=0x%x mem_size=%d", - info.root_ppn, - info.kstack_top, - info.memory_size - ); - } else { - log!("boot_info missing; kernel task not initialized"); - } + init_kernel(state_ptr, state_len, boot_info_ptr); let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; @@ -193,10 +144,11 @@ fn program_call(tx: &Transaction) { let kstack_top = unsafe { BOOT_INFO.get_mut().as_ref().map(|b| b.kstack_top).unwrap_or(0) }; - if let Some(task) = launch_program(0, kstack_top) { + if let Some(task) = launch_program(kstack_top, &tx.to, &tx.from, &account.code, &tx.data) { logf!( - "Program task created: root=0x%x window_size=%d", + "Program task created: root=0x%x asid=%d window_size=%d", task.addr_space.root_ppn, + task.addr_space.asid as u32, PROGRAM_WINDOW_BYTES as u32 ); unsafe { diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/mmu.rs index e70b6ba..39264ac 100644 --- a/crates/kernel/src/mmu.rs +++ b/crates/kernel/src/mmu.rs @@ -1,32 +1,17 @@ -use crate::global::Global; +use core::{cmp, marker::PhantomData, ptr}; + +use crate::global::{PAGE_ALLOC, ROOT_PPN}; use crate::BootInfo; +use types::{ + Sv32PagePerms, Sv32PageTable, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE, SV32_VPN_MASK, map_allocating, + SV32_PTE_R, SV32_PTE_W, SV32_PTE_X, SV32_PTE_V, +}; -const PAGE_SIZE: usize = 4096; -const VPN_MASK: u32 = 0x3ff; -const PTE_V: u32 = 1 << 0; -const PTE_R: u32 = 1 << 1; -const PTE_W: u32 = 1 << 2; -const PTE_X: u32 = 1 << 3; -const PTE_U: u32 = 1 << 4; - -#[derive(Clone, Copy)] -pub struct PagePerms { - pub read: bool, - pub write: bool, - pub exec: bool, - pub user: bool, -} +const PAGE_SIZE: usize = SV32_PAGE_SIZE; +const DIRECT_MAP_BASE: usize = SV32_DIRECT_MAP_BASE as usize; -impl PagePerms { - pub const fn user_rwx() -> Self { - Self { - read: true, - write: true, - exec: true, - user: true, - } - } -} +/// Permissions used by the kernel/user mapping helpers. +pub type PagePerms = Sv32PagePerms; #[derive(Debug, Clone, Copy)] pub struct PageAllocator { @@ -53,17 +38,22 @@ impl PageAllocator { Some(ppn) } - /// Zero a 4 KiB page in guest physical memory. + /// Zero a 4 KiB page in guest physical memory via the direct map. fn zero_page(ppn: u32) { - let base = (ppn as usize) * PAGE_SIZE; + let base = (ppn as usize) + .checked_mul(PAGE_SIZE) + .expect("page offset overflow"); + let virt = direct_map_addr(base).expect("direct map overflow while zeroing page"); unsafe { - core::ptr::write_bytes(base as *mut u8, 0, PAGE_SIZE); + ptr::write_bytes(virt as *mut u8, 0, PAGE_SIZE); } } } -static ROOT_PPN: Global = Global::new(0); -static PAGE_ALLOC: Global> = Global::new(None); +/// Return the kernel's current root PPN (satp PPN field). +pub fn current_root() -> u32 { + unsafe { *ROOT_PPN.get_mut() } +} /// Initialize the kernel MMU allocator state from bootloader handoff. pub fn init(boot_info: &BootInfo) { @@ -74,97 +64,142 @@ pub fn init(boot_info: &BootInfo) { } } -/// Map a user-visible virtual range with the provided permissions into the current root. -pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { - let root = unsafe { *ROOT_PPN.get_mut() }; +/// Allocate and zero a fresh L1 root page table. Returns None if out of frames. +pub fn alloc_root() -> Option { let alloc = unsafe { PAGE_ALLOC.get_mut() }; match alloc { - Some(alloc) => map_range(root, va_start, len, perms, alloc), + Some(alloc) => { + let root = alloc.alloc()?; + PageAllocator::zero_page(root); + Some(root) + } + None => None, + } +} + +/// Map a user-visible virtual range with the provided permissions into a specific root. +pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let mapper = KernelMapper::new(alloc); + map_allocating(&mapper, root_ppn, va_start, len, perms) + } None => false, } } -fn map_range(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms, alloc: &mut PageAllocator) -> bool { - if len == 0 { - return true; +/// Map a user-visible virtual range with the provided permissions into the current root. +pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { + let root = unsafe { *ROOT_PPN.get_mut() }; + map_user_range_for_root(root, va_start, len, perms) +} + +/// Walk Sv32 to translate a VA in the given root to a physical address. +pub fn translate_user_va(root_ppn: u32, va: u32) -> Option { + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + let offset = (va & 0xfff) as usize; + + let l1_base = (root_ppn as usize) + .checked_mul(PAGE_SIZE)?; + let l1_addr = l1_base + vpn1 as usize * core::mem::size_of::(); + let l1_pte = read_pte(l1_addr)?; + if l1_pte & SV32_PTE_V == 0 || l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return None; } - let start = align_down(va_start as usize, PAGE_SIZE) as u32; - let end = (va_start as usize).saturating_add(len); - let end_aligned = align_up(end, PAGE_SIZE) as u32; - let mut va = start; - while va < end_aligned { - if !map_page(root_ppn, va, perms, alloc) { - return false; - } - va = va.wrapping_add(PAGE_SIZE as u32); + let l2_base = ((l1_pte >> 10) as usize) + .checked_mul(PAGE_SIZE)?; + let l2_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + let l2_pte = read_pte(l2_addr)?; + if l2_pte & SV32_PTE_V == 0 { + return None; } - true + + let ppn = (l2_pte >> 10) as usize; + ppn.checked_mul(PAGE_SIZE)?.checked_add(offset) } -/// Map a single 4 KiB page at `va` into the two-level Sv32 page tables rooted at `root_ppn`. -/// Allocates an L2 table if the L1 entry is absent; refuses superpages; fills in a leaf PTE -/// with the requested permissions after zeroing the backing frame. -fn map_page(root_ppn: u32, va: u32, perms: PagePerms, alloc: &mut PageAllocator) -> bool { - let vpn1 = (va >> 22) & VPN_MASK; - let vpn0 = (va >> 12) & VPN_MASK; - - // L1 lookup - let root_base = (root_ppn as usize) * PAGE_SIZE; - let l1_entry_addr = root_base + vpn1 as usize * core::mem::size_of::(); - let mut l1_pte = unsafe { (l1_entry_addr as *const u32).read_volatile() }; - - if l1_pte & PTE_V == 0 { - let l2 = match alloc.alloc() { - Some(ppn) => ppn, +/// Copy data into a user VA range for a specific root using the direct-map window. +pub fn copy_into_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { + if data.is_empty() { + return true; + } + let mut remaining = data.len(); + let mut src_off = 0usize; + let mut va = va_start; + while remaining > 0 { + let phys = match translate_user_va(root_ppn, va) { + Some(p) => p, None => return false, }; - PageAllocator::zero_page(l2); - l1_pte = ((l2 as u32) << 10) | PTE_V; - unsafe { (l1_entry_addr as *mut u32).write_volatile(l1_pte) }; - } else if l1_pte & (PTE_R | PTE_W | PTE_X) != 0 { - // Superpages not supported. - return false; + let page_off = (va as usize) & (PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, PAGE_SIZE - page_off); + let dst = match direct_map_addr(phys) { + Some(v) => v, + None => return false, + }; + unsafe { + ptr::copy_nonoverlapping( + data.as_ptr().add(src_off), + dst as *mut u8, + to_copy, + ); + } + remaining -= to_copy; + src_off += to_copy; + va = va.wrapping_add(to_copy as u32); } + true +} - let l2_ppn = l1_pte >> 10; - let l2_base = (l2_ppn as usize) * PAGE_SIZE; - let l2_entry_addr = l2_base + vpn0 as usize * core::mem::size_of::(); +/// Sv32 page-table accessor that routes PTE traffic through the kernel's direct map. +struct KernelMapper<'a> { + alloc: *mut PageAllocator, + _marker: PhantomData<&'a mut PageAllocator>, +} - let existing = unsafe { (l2_entry_addr as *const u32).read_volatile() }; - if existing & PTE_V != 0 { - // Already mapped. - return true; +impl<'a> KernelMapper<'a> { + fn new(alloc: &'a mut PageAllocator) -> Self { + Self { + alloc: alloc as *mut PageAllocator, + _marker: PhantomData, + } } +} - let frame = match alloc.alloc() { - Some(ppn) => ppn, - None => return false, - }; - PageAllocator::zero_page(frame); +impl<'a> Sv32PageTable for KernelMapper<'a> { + fn page_size(&self) -> usize { + PAGE_SIZE + } - let mut flags = PTE_V; - if perms.read { - flags |= PTE_R; + fn read_pte(&self, phys_addr: usize) -> Option { + let va = direct_map_addr(phys_addr)?; + Some(unsafe { (va as *const u32).read_volatile() }) } - if perms.write { - flags |= PTE_W; + + fn write_pte(&self, phys_addr: usize, val: u32) { + if let Some(va) = direct_map_addr(phys_addr) { + unsafe { (va as *mut u32).write_volatile(val) }; + } } - if perms.exec { - flags |= PTE_X; + + fn alloc_frame(&self) -> Option { + let alloc = unsafe { &mut *self.alloc }; + alloc.alloc() } - if perms.user { - flags |= PTE_U; + + fn zero_frame(&self, ppn: u32) { + PageAllocator::zero_page(ppn); } - let leaf = ((frame as u32) << 10) | flags; - unsafe { (l2_entry_addr as *mut u32).write_volatile(leaf) }; - true } -const fn align_up(val: usize, align: usize) -> usize { - (val + (align - 1)) & !(align - 1) +fn direct_map_addr(phys: usize) -> Option { + DIRECT_MAP_BASE.checked_add(phys) } -const fn align_down(val: usize, align: usize) -> usize { - val & !(align - 1) +fn read_pte(phys_addr: usize) -> Option { + let va = direct_map_addr(phys_addr)?; + Some(unsafe { (va as *const u32).read_volatile() }) } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index f7e8723..50e530e 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -21,6 +21,9 @@ pub use transaction::*; pub mod boot; pub use boot::BootInfo; +pub mod mmu; +pub use mmu::*; + // used for serialization pub trait SerializeField { /// Appends `self` into `buf` at `*offset`, advancing the offset. diff --git a/crates/types/src/mmu.rs b/crates/types/src/mmu.rs new file mode 100644 index 0000000..f529b66 --- /dev/null +++ b/crates/types/src/mmu.rs @@ -0,0 +1,250 @@ +#![allow(dead_code)] + +use core::convert::TryFrom; +use core::mem; + +/// Sv32 page size in bytes (4 KiB). +pub const SV32_PAGE_SIZE: usize = 4096; +/// Number of bits in a VPN index. +pub const SV32_VPN_MASK: u32 = 0x3ff; + +/// Sv32 PTE flag bits. +pub const SV32_PTE_V: u32 = 1 << 0; +pub const SV32_PTE_R: u32 = 1 << 1; +pub const SV32_PTE_W: u32 = 1 << 2; +pub const SV32_PTE_X: u32 = 1 << 3; +pub const SV32_PTE_U: u32 = 1 << 4; + +/// Sv32 satp PPN mask (bits 21:0). Mode bits are ignored in this emulator. +pub const SV32_SATP_PPN_MASK: u32 = 0x003f_ffff; + +/// Virtual base used to directly map all guest physical memory for the kernel. +/// This keeps physical page-table frames accessible even after paging is enabled. +pub const SV32_DIRECT_MAP_BASE: u32 = 0x4000_0000; + +/// Simple permission descriptor for Sv32 mappings. +#[derive(Clone, Copy, Debug)] +pub struct Sv32PagePerms { + pub read: bool, + pub write: bool, + pub exec: bool, + pub user: bool, +} + +impl Sv32PagePerms { + pub const fn new(read: bool, write: bool, exec: bool, user: bool) -> Self { + Self { + read, + write, + exec, + user, + } + } + + pub const fn user_rwx() -> Self { + Self::new(true, true, true, true) + } + + pub const fn kernel_rw() -> Self { + Self::new(true, true, false, false) + } + + pub const fn kernel_rwx() -> Self { + Self::new(true, true, true, false) + } + + fn to_pte_flags(self) -> u32 { + let mut flags = SV32_PTE_V; + if self.read { + flags |= SV32_PTE_R; + } + if self.write { + flags |= SV32_PTE_W; + } + if self.exec { + flags |= SV32_PTE_X; + } + if self.user { + flags |= SV32_PTE_U; + } + flags + } +} + +/// Abstraction for Sv32 page-table manipulation. +/// +/// Implementations provide raw PTE reads/writes at physical addresses, frame +/// allocation, and zeroing. Mapping helpers below drive the common Sv32 walk +/// for both the bootloader and kernel. +pub trait Sv32PageTable { + fn page_size(&self) -> usize { + SV32_PAGE_SIZE + } + + fn read_pte(&self, phys_addr: usize) -> Option; + fn write_pte(&self, phys_addr: usize, val: u32); + fn alloc_frame(&self) -> Option; + fn zero_frame(&self, ppn: u32); +} + +/// Map a virtual range by allocating fresh physical frames for leaves. +/// +/// Returns false on allocation/overflow failures or unsupported superpage cases. +pub fn map_allocating( + pt: &T, + root_ppn: u32, + va_start: u32, + len: usize, + perms: Sv32PagePerms, +) -> bool { + map_range_internal(pt, root_ppn, va_start, len, perms, LeafStrategy::Allocate) +} + +/// Map a virtual range to an existing physical range (no leaf allocation). +/// +/// `phys_start` must be page aligned. Returns false on failure. +pub fn map_to_physical( + pt: &T, + root_ppn: u32, + va_start: u32, + phys_start: u32, + len: usize, + perms: Sv32PagePerms, +) -> bool { + if phys_start as usize % pt.page_size() != 0 { + return false; + } + map_range_internal( + pt, + root_ppn, + va_start, + len, + perms, + LeafStrategy::PhysCursor { + next_phys: phys_start, + }, + ) +} + +#[derive(Clone, Copy)] +enum LeafStrategy { + Allocate, + PhysCursor { next_phys: u32 }, +} + +fn map_range_internal( + pt: &T, + root_ppn: u32, + va_start: u32, + len: usize, + perms: Sv32PagePerms, + mut strategy: LeafStrategy, +) -> bool { + if len == 0 { + return true; + } + + let page_size = pt.page_size(); + let start = align_down(va_start as usize, page_size); + let end = match (va_start as usize).checked_add(len) { + Some(v) => align_up(v, page_size), + None => return false, + }; + + let mut va = start as u32; + while (va as usize) < end { + let phys_override = match &mut strategy { + LeafStrategy::Allocate => None, + LeafStrategy::PhysCursor { next_phys } => { + let phys = *next_phys; + *next_phys = next_phys.wrapping_add(page_size as u32); + Some(phys) + } + }; + + if !map_page(pt, root_ppn, va, perms, phys_override) { + return false; + } + va = va.wrapping_add(page_size as u32); + } + true +} + +fn map_page( + pt: &T, + root_ppn: u32, + va: u32, + perms: Sv32PagePerms, + phys_override: Option, +) -> bool { + let page_size = pt.page_size(); + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + + let root_base = match (root_ppn as usize).checked_mul(page_size) { + Some(base) => base, + None => return false, + }; + let l1_entry_addr = root_base + vpn1 as usize * mem::size_of::(); + let mut l1_pte = match pt.read_pte(l1_entry_addr) { + Some(pte) => pte, + None => return false, + }; + + if l1_pte & SV32_PTE_V == 0 { + let l2 = match pt.alloc_frame() { + Some(ppn) => ppn, + None => return false, + }; + pt.zero_frame(l2); + l1_pte = (l2 << 10) | SV32_PTE_V; + pt.write_pte(l1_entry_addr, l1_pte); + } else if l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + // Superpages are not supported. + return false; + } + + let l2_base = match usize::try_from(l1_pte >> 10) + .ok() + .and_then(|ppn| ppn.checked_mul(page_size)) + { + Some(base) => base, + None => return false, + }; + let l2_entry_addr = l2_base + vpn0 as usize * mem::size_of::(); + + if let Some(existing) = pt.read_pte(l2_entry_addr) { + if existing & SV32_PTE_V != 0 { + // Already mapped. + return true; + } + } + + let leaf_ppn = match phys_override { + Some(phys) => { + if (phys as usize) % page_size != 0 { + return false; + } + phys / page_size as u32 + } + None => match pt.alloc_frame() { + Some(ppn) => { + pt.zero_frame(ppn); + ppn + } + None => return false, + }, + }; + + let leaf_pte = (leaf_ppn << 10) | perms.to_pte_flags(); + pt.write_pte(l2_entry_addr, leaf_pte); + true +} + +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +const fn align_down(val: usize, align: usize) -> usize { + val & !(align - 1) +} From 12dd5ccced3cbbab780635f9001732b60c49be0f Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 23 Dec 2025 19:39:16 +0200 Subject: [PATCH 23/70] [WIP] calling program works! --- crates/kernel/src/launch.rs | 319 +++++++++++++++++++++++++++++++++++- crates/kernel/src/lib.rs | 2 +- crates/kernel/src/main.rs | 29 +++- crates/kernel/src/mmu.rs | 148 ++++++++++++++++- 4 files changed, 489 insertions(+), 9 deletions(-) diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index 9f22346..938531a 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -1,7 +1,50 @@ #![allow(dead_code)] +// Program launch flow (kernel side) +// --------------------------------- +// Goals: +// - Create a fresh address space for each program call (new root PPN + ASID). +// - Map a fixed, contiguous user window starting at VA 0x0 that holds: +// * Code/rodata (program bytes copied starting at VA 0x0; entry at `entry_off`) +// * A user stack (STACK_BYTES) +// * A user heap (HEAP_BYTES) with input placed at INPUT_BASE_ADDR +// - Copy call arguments (to/from addresses + input buffer) into that window. +// - Prepare a trapframe with PC/SP/args and transfer control to user code. +// +// Key pieces: +// - PROGRAM_WINDOW_BYTES covers code + rodata + stack + heap: a single map call per program. +// - TRAMPOLINE_VA is one page immediately after the user window, mapped into both +// the kernel root and the new user root. It contains two instructions: +// csrw satp, t0 +// jr t1 +// This lets us switch satp safely from a VA that stays valid across the root change. +// +// prep_program_task(kstack_top, to, from, code, input, entry_off): +// 1) Allocate ASID and a fresh root PPN; map the user window with user_rwx perms. +// 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). +// 3) Map the trampoline page into the user root and mirror the same physical page +// into the current kernel root; write TRAMPOLINE_CODE into it. +// 4) Build a Task with AddressSpace {root_ppn, asid} and set trapframe: +// pc = PROGRAM_VA_BASE + entry_off +// sp = top of user stack within the window +// a0..a3 = to/from/input_base/input_len +// Caller can push the task into TASKS for bookkeeping. +// +// run_task(task): +// - Save the current kernel frame (sp/ra/pc) into TASKS[0] for a future return path. +// - Preload t0 with the task root (satp value) and t1 with the user PC; load +// user sp and a0..a3; clear ra. +// - jr TRAMPOLINE_VA. The trampoline executes under the old root, writes satp +// to the new root, and immediately jr t1 into user code. There is no return +// path yet; this is a one-way handoff. +// +// Notes: +// - The window and trampoline VAs are low for simplicity; nothing here relocates. +// - We currently do not touch sstatus/mstatus or perform sfence.vma; add those +// when modeling fuller privilege transitions. + use crate::{AddressSpace, Config, Task, mmu}; -use crate::global::NEXT_ASID; +use crate::global::{BOOT_INFO, NEXT_ASID, TASKS}; use program::log; use program::logf; use types::{address::Address, ADDRESS_LEN}; @@ -10,6 +53,12 @@ const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap pub const PROGRAM_VA_BASE: u32 = 0x0; +const SYSCALL_BRK: u32 = 214; +// Location of the page that hosts the satp-switch trampoline. Kept just past +// the user window so it does not collide with program text/stack/heap. This VA +// is mapped into both roots so the satp write does not invalidate the +// instruction stream mid-flight. +const TRAMPOLINE_VA: u32 = (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES) as u32; const fn align_up(val: usize, align: usize) -> usize { (val + (align - 1)) & !(align - 1) } @@ -21,10 +70,21 @@ pub const PROGRAM_WINDOW_BYTES: usize = align_up( ); const REG_SP: usize = 2; +const REG_RA: usize = 1; const REG_A0: usize = 10; const REG_A1: usize = 11; const REG_A2: usize = 12; const REG_A3: usize = 13; +// Raw RISC-V words for the trampoline used to switch satp safely while +// executing from a page mapped in both the kernel and user roots. The kernel +// loads t0 = target satp and t1 = user PC before entering this stub so we can +// change roots and immediately branch to user code without returning to +// unmapped kernel text. +// t0: target satp value, t1: user PC (jump target). +const TRAMPOLINE_CODE: [u32; 2] = [ + 0x1802_9073, // csrw satp, t0 + 0x0003_0067, // jr t1 +]; const TO_PTR_ADDR: u32 = 0x120; const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; @@ -38,18 +98,55 @@ const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; /// /// The caller is responsible for copying program bytes into the mapped window /// and initializing the user trapframe (PC/SP/args) before running. -pub fn launch_program( +pub fn prep_program_task( kstack_top: u32, to: &Address, from: &Address, code: &[u8], input: &[u8], + entry_off: u32, ) -> Option { if input.len() > Config::MAX_INPUT_LEN { log!("launch_program: input too large"); return None; } + // Make sure the kernel page allocator does not hand out frames that the + // kernel heap already consumed (account code lives there). We query the + // current program break via brk(0) and bump the allocator past that PPN. + let heap_brk = current_heap_break(); + if let Some(heap_phys) = mmu::translate_user_va(mmu::current_root(), heap_brk.saturating_sub(1)) { + let min_ppn = ((heap_phys / PAGE_SIZE) as u32).saturating_add(1); + mmu::bump_page_allocator(min_ppn); + logf!( + "prep_program_task: bump page alloc to ppn=0x%x from heap_brk=0x%x phys=0x%x", + min_ppn, + heap_brk, + heap_phys as u32 + ); + } else { + // Fallback: push the allocator toward the top of memory so we avoid heap overlap. + let total_ppn = unsafe { + BOOT_INFO + .get_mut() + .as_ref() + .map(|b| b.memory_size / PAGE_SIZE as u32) + .unwrap_or(0) + }; + let reserve = (PROGRAM_WINDOW_BYTES / PAGE_SIZE) as u32 + 4; // user window + a few tables + if total_ppn > reserve { + let min_ppn = total_ppn - reserve; + mmu::bump_page_allocator(min_ppn); + logf!( + "prep_program_task: fallback bump page alloc to ppn=0x%x (total_ppn=0x%x)", + min_ppn, + total_ppn + ); + } else { + logf!("prep_program_task: could not translate heap_brk=0x%x", heap_brk); + } + } + let asid = alloc_asid(); let root_ppn = match mmu::alloc_root() { Some(ppn) => ppn, @@ -58,6 +155,14 @@ pub fn launch_program( return None; } }; + logf!( + "prep_program_task: new root=0x%x asid=%d code_len=%d input_len=%d entry_off=0x%x", + root_ppn, + asid as u32, + code.len() as u32, + input.len() as u32, + entry_off, + ); let window_end = PROGRAM_VA_BASE.wrapping_add(PROGRAM_WINDOW_BYTES as u32); logf!( "launch_program: asid=%d root=0x%x map=[0x%x,0x%x)", @@ -72,11 +177,53 @@ pub fn launch_program( return None; } - // Copy code and arguments into user memory. - if !mmu::copy_into_user(root_ppn, Config::PROGRAM_START_ADDR, code) { + // Copy the full program image starting at VA 0 so section offsets (e.g. .text at 0x400) + // land where the ELF expected them. Entry offset is provided by the caller. + if entry_off as usize >= code.len() { + logf!( + "prep_program_task: entry_off 0x%x is outside code len %d", + entry_off, + code.len() as u32 + ); + return None; + } + if code.len() >= entry_off as usize + 8 { + let head = u32::from_le_bytes([ + code[entry_off as usize], + code[entry_off as usize + 1], + code[entry_off as usize + 2], + code[entry_off as usize + 3], + ]); + let head2 = u32::from_le_bytes([ + code[entry_off as usize + 4], + code[entry_off as usize + 5], + code[entry_off as usize + 6], + code[entry_off as usize + 7], + ]); + logf!( + "prep_program_task: code head[0..8]=0x%x 0x%x (entry_off=0x%x)", + head, + head2, + entry_off, + ); + } + let nz_count = code.iter().filter(|&&b| b != 0).count(); + let local_first_nz = code.iter().position(|&b| b != 0).unwrap_or(code.len()); + logf!( + "prep_program_task: local code stats first_nz=0x%x nz_count=%d", + local_first_nz as u32, + nz_count as u32 + ); + if !mmu::copy_into_user(root_ppn, PROGRAM_VA_BASE, code) { logf!("launch_program: failed to copy code into root=0x%x", root_ppn); return None; } + logf!( + "prep_program_task: copied code to 0x%x len=%d nz_count=%d", + PROGRAM_VA_BASE, + code.len() as u32, + nz_count as u32 + ); if !mmu::copy_into_user(root_ppn, TO_PTR_ADDR, &to.0) { logf!("launch_program: failed to copy 'to' address into root=0x%x", root_ppn); return None; @@ -89,21 +236,165 @@ pub fn launch_program( logf!("launch_program: failed to copy input into root=0x%x", root_ppn); return None; } + logf!( + "prep_program_task: copied args to=0x%x from=0x%x input=0x%x len=%d", + TO_PTR_ADDR, + FROM_PTR_ADDR, + INPUT_BASE_ADDR, + input.len() as u32 + ); + + // Sanity check where the code landed in the user root. + let entry_va = PROGRAM_VA_BASE.wrapping_add(entry_off); + let user_phys = mmu::translate_user_va(root_ppn, entry_va).unwrap_or(usize::MAX); + let user_word = mmu::peek_word(root_ppn, entry_va).unwrap_or(0); + logf!( + "prep_program_task: code VA=0x%x user_phys=0x%x user_word=0x%x code_start=0x%x", + entry_va, + user_phys as u32, + user_word, + entry_off + ); + // Install a small trampoline page mapped in both roots so we can switch + // satp safely before jumping into the user program. + let tramp_perms = mmu::PagePerms::user_rwx(); + if !mmu::map_user_range_for_root(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { + logf!("prep_program_task: failed to map trampoline page in user root"); + return None; + } + let tramp_phys = match mmu::translate_user_va(root_ppn, TRAMPOLINE_VA) { + Some(p) => p as u32, + None => { + log!("prep_program_task: trampoline VA not mapped"); + return None; + } + }; + if !mmu::map_physical_range_for_root( + mmu::current_root(), + TRAMPOLINE_VA, + tramp_phys, + PAGE_SIZE, + tramp_perms, + ) { + log!("prep_program_task: failed to mirror trampoline into kernel root"); + return None; + } + let mut tramp_bytes = [0u8; TRAMPOLINE_CODE.len() * 4]; + for (i, word) in TRAMPOLINE_CODE.iter().enumerate() { + tramp_bytes[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); + } + if !mmu::copy_into_user(root_ppn, TRAMPOLINE_VA, &tramp_bytes) { + log!("prep_program_task: failed to populate trampoline code"); + return None; + } + let tramp_phys_2 = mmu::translate_user_va(root_ppn, TRAMPOLINE_VA + 4).unwrap_or(usize::MAX); + logf!( + "prep_program_task: trampoline mapped va=0x%x phys=0x%x phys(+4)=0x%x", + TRAMPOLINE_VA, + tramp_phys, + tramp_phys_2 as u32 + ); let mut task = Task::new(AddressSpace::new(root_ppn, asid), kstack_top); // Set up initial trapframe. let stack_top = PROGRAM_VA_BASE .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); - task.tf.pc = Config::PROGRAM_START_ADDR; + task.tf.pc = entry_va; task.tf.regs[REG_SP] = stack_top; task.tf.regs[REG_A0] = TO_PTR_ADDR; task.tf.regs[REG_A1] = FROM_PTR_ADDR; task.tf.regs[REG_A2] = INPUT_BASE_ADDR; task.tf.regs[REG_A3] = input.len() as u32; + logf!( + "prep_program_task: trapframe pc=0x%x sp=0x%x a0=0x%x a1=0x%x a2=0x%x a3=%d", + task.tf.pc, + task.tf.regs[REG_SP], + task.tf.regs[REG_A0], + task.tf.regs[REG_A1], + task.tf.regs[REG_A2], + task.tf.regs[REG_A3], + ); + // Also log the expected user stack window for sanity. + let stack_base = stack_top.saturating_sub(STACK_BYTES as u32); + logf!( + "prep_program_task: stack window=[0x%x,0x%x) heap_base=0x%x", + stack_base, + stack_top, + Config::HEAP_START_ADDR as u32 + ); Some(task) } +/// One-way context switch into a user task: +/// - Saves the current kernel frame into TASKS[0] +/// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) +pub fn run_task(task: &Task) { + let kernel_root = mmu::current_root(); + let target_root = task.addr_space.root_ppn; + logf!( + "run_task: switching satp 0x%x -> 0x%x asid=%d pc=0x%x sp=0x%x", + kernel_root, + target_root, + task.addr_space.asid as u32, + task.tf.pc, + task.tf.regs[REG_SP], + ); + // Save the current kernel frame (SP/RA/PC) into the kernel task slot (index 0). + let mut saved_sp: u32; + let mut saved_ra: u32; + let mut saved_pc: u32; + unsafe { + core::arch::asm!("mv {out}, sp", out = out(reg) saved_sp); + core::arch::asm!("mv {out}, ra", out = out(reg) saved_ra); + core::arch::asm!("auipc {out}, 0", out = out(reg) saved_pc); + } + logf!( + "run_task: saved kernel frame sp=0x%x ra=0x%x pc=0x%x", + saved_sp, + saved_ra, + saved_pc + ); + // Stash the kernel context so a future return path could restore it. + unsafe { + if let Some(tasks) = TASKS.get_mut() { + if let Some(kernel_task) = tasks.get_mut(0) { + kernel_task.addr_space.root_ppn = kernel_root; + kernel_task.tf.regs[REG_SP] = saved_sp; + kernel_task.tf.regs[REG_RA] = saved_ra; + kernel_task.tf.pc = saved_pc; + } + } + } + // Update the helper's view of the current root before switching. + mmu::set_current_root(target_root); + // Set up registers and jump to the shared trampoline page (mapped in both + // the kernel and user roots). The trampoline will write satp and transfer + // control to the user PC. + unsafe { + core::arch::asm!( + "mv t0, {satp}", // satp to write + "mv t1, {pc}", // user PC + "mv ra, zero", + "mv sp, {sp}", + "mv a0, {a0}", + "mv a1, {a1}", + "mv a2, {a2}", + "mv a3, {a3}", + "jr {tramp}", + satp = in(reg) target_root, + pc = in(reg) task.tf.pc, + sp = in(reg) task.tf.regs[REG_SP], + a0 = in(reg) task.tf.regs[REG_A0], + a1 = in(reg) task.tf.regs[REG_A1], + a2 = in(reg) task.tf.regs[REG_A2], + a3 = in(reg) task.tf.regs[REG_A3], + tramp = in(reg) TRAMPOLINE_VA, + options(noreturn) + ); + } +} + fn alloc_asid() -> u16 { unsafe { let counter = NEXT_ASID.get_mut(); @@ -112,3 +403,21 @@ fn alloc_asid() -> u16 { asid } } + +/// Query the current heap break via the `brk(2)` syscall (a7 = 214). +fn current_heap_break() -> u32 { + let mut brk: u32; + unsafe { + core::arch::asm!( + "mv a0, zero", + "li a7, {sys_brk}", + "ecall", + out("a0") brk, + sys_brk = const SYSCALL_BRK, + out("a1") _, out("a2") _, out("a3") _, out("a4") _, out("a5") _, out("a6") _, + out("t0") _, out("t1") _, out("t2") _, + options(nostack) + ); + } + brk +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index c8d4f40..09b68fe 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -7,5 +7,5 @@ pub mod global; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; pub mod launch; -pub use launch::{launch_program, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; +pub use launch::{prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; pub mod mmu; diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 307e921..bc7ae78 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -6,7 +6,7 @@ extern crate alloc; use alloc::{format, vec}; use core::mem::forget; use core::slice; -use kernel::{BootInfo, Config, launch_program, PROGRAM_WINDOW_BYTES}; +use kernel::{BootInfo, Config, prep_program_task, run_task, PROGRAM_WINDOW_BYTES}; use program::{log, logf}; use state::State; use types::transaction::{Transaction, TransactionBundle, TransactionType}; @@ -122,6 +122,22 @@ fn program_call(tx: &Transaction) { return; } + let first_nz = account + .code + .iter() + .position(|&b| b != 0) + .unwrap_or(account.code.len()); + let nz_count = account.code.iter().filter(|&&b| b != 0).count(); + logf!( + "%s", + display: format!( + "Program code stats: len={} first_nz={} nz_count={}", + account.code.len(), + first_nz, + nz_count + ) + ); + let code_len = account.code.len(); let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; if code_len > max { @@ -144,7 +160,11 @@ fn program_call(tx: &Transaction) { let kstack_top = unsafe { BOOT_INFO.get_mut().as_ref().map(|b| b.kstack_top).unwrap_or(0) }; - if let Some(task) = launch_program(kstack_top, &tx.to, &tx.from, &account.code, &tx.data) { + let entry_off = first_nz as u32; + + if let Some(task) = + prep_program_task(kstack_top, &tx.to, &tx.from, &account.code, &tx.data, entry_off) + { logf!( "Program task created: root=0x%x asid=%d window_size=%d", task.addr_space.root_ppn, @@ -157,6 +177,11 @@ fn program_call(tx: &Transaction) { Some(tasks) => tasks.push(task), None => *tasks_slot = Some(vec![task]), } + if let Some(tasks) = tasks_slot { + if let Some(last) = tasks.last() { + run_task(last); + } + } } } else { log!("Program call skipped: no memory manager installed"); diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/mmu.rs index 39264ac..c4c5e4b 100644 --- a/crates/kernel/src/mmu.rs +++ b/crates/kernel/src/mmu.rs @@ -4,7 +4,7 @@ use crate::global::{PAGE_ALLOC, ROOT_PPN}; use crate::BootInfo; use types::{ Sv32PagePerms, Sv32PageTable, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE, SV32_VPN_MASK, map_allocating, - SV32_PTE_R, SV32_PTE_W, SV32_PTE_X, SV32_PTE_V, + SV32_PTE_R, SV32_PTE_W, SV32_PTE_X, SV32_PTE_V, SV32_PTE_U, map_to_physical, }; const PAGE_SIZE: usize = SV32_PAGE_SIZE; @@ -48,6 +48,13 @@ impl PageAllocator { ptr::write_bytes(virt as *mut u8, 0, PAGE_SIZE); } } + + /// Advance the allocator so it will not hand out frames below `min_ppn`. + pub fn bump_to(&mut self, min_ppn: u32) { + if self.next_ppn < min_ppn { + self.next_ppn = min_ppn; + } + } } /// Return the kernel's current root PPN (satp PPN field). @@ -55,6 +62,13 @@ pub fn current_root() -> u32 { unsafe { *ROOT_PPN.get_mut() } } +/// Update the current root PPN used by kernel helpers. +pub fn set_current_root(root_ppn: u32) { + unsafe { + *ROOT_PPN.get_mut() = root_ppn; + } +} + /// Initialize the kernel MMU allocator state from bootloader handoff. pub fn init(boot_info: &BootInfo) { unsafe { @@ -77,6 +91,15 @@ pub fn alloc_root() -> Option { } } +/// Ensure the page allocator will not hand out frames below `min_ppn`. +pub fn bump_page_allocator(min_ppn: u32) { + unsafe { + if let Some(alloc) = PAGE_ALLOC.get_mut() { + alloc.bump_to(min_ppn); + } + } +} + /// Map a user-visible virtual range with the provided permissions into a specific root. pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { let alloc = unsafe { PAGE_ALLOC.get_mut() }; @@ -95,6 +118,56 @@ pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { map_user_range_for_root(root, va_start, len, perms) } +/// Map a VA range in `root_ppn` to an explicit physical range (no allocation). +pub fn map_physical_range_for_root( + root_ppn: u32, + va_start: u32, + phys_start: u32, + len: usize, + perms: PagePerms, +) -> bool { + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let mapper = KernelMapper::new(alloc); + map_to_physical(&mapper, root_ppn, va_start, phys_start, len, perms) + } + None => false, + } +} + +/// Mirror a mapped user range from `user_root` into the current kernel root so the +/// kernel can execute the user program without switching satp. +pub fn mirror_user_range_into_kernel(user_root: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + if len == 0 { + return true; + } + let page_size = PAGE_SIZE; + let start = align_down_local(va_start as usize, page_size) as u32; + let end = match (va_start as usize).checked_add(len) { + Some(v) => align_up_local(v, page_size) as u32, + None => return false, + }; + let kernel_root = current_root(); + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + let mapper_alloc = match alloc { + Some(a) => a, + None => return false, + }; + let mut va = start; + while va < end { + let phys = match translate_user_va(user_root, va) { + Some(p) => p as u32, + None => return false, + }; + if !overwrite_map_page(kernel_root, va, phys, perms, mapper_alloc) { + return false; + } + va = va.wrapping_add(page_size as u32); + } + true +} + /// Walk Sv32 to translate a VA in the given root to a physical address. pub fn translate_user_va(root_ppn: u32, va: u32) -> Option { let vpn1 = (va >> 22) & SV32_VPN_MASK; @@ -121,6 +194,13 @@ pub fn translate_user_va(root_ppn: u32, va: u32) -> Option { ppn.checked_mul(PAGE_SIZE)?.checked_add(offset) } +/// Peek a 32-bit value at a VA in a given root using the direct-map window. +pub fn peek_word(root_ppn: u32, va: u32) -> Option { + let phys = translate_user_va(root_ppn, va)?; + let va_ptr = direct_map_addr(phys)?; + Some(unsafe { (va_ptr as *const u32).read_volatile() }) +} + /// Copy data into a user VA range for a specific root using the direct-map window. pub fn copy_into_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { if data.is_empty() { @@ -203,3 +283,69 @@ fn read_pte(phys_addr: usize) -> Option { let va = direct_map_addr(phys_addr)?; Some(unsafe { (va as *const u32).read_volatile() }) } + +fn write_pte(phys_addr: usize, val: u32) { + if let Some(va) = direct_map_addr(phys_addr) { + unsafe { (va as *mut u32).write_volatile(val) }; + } +} + +const fn align_up_local(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +const fn align_down_local(val: usize, align: usize) -> usize { + val & !(align - 1) +} + +fn overwrite_map_page( + root_ppn: u32, + va: u32, + phys_start: u32, + perms: PagePerms, + alloc: &mut PageAllocator, +) -> bool { + let page_size = PAGE_SIZE; + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + + let root_base = (root_ppn as usize) + .checked_mul(page_size) + .unwrap(); + let l1_addr = root_base + vpn1 as usize * core::mem::size_of::(); + let mut l1_pte = read_pte(l1_addr).unwrap_or(0); + if l1_pte & SV32_PTE_V == 0 { + let l2 = match alloc.alloc() { + Some(ppn) => ppn, + None => return false, + }; + PageAllocator::zero_page(l2); + l1_pte = (l2 << 10) | SV32_PTE_V; + write_pte(l1_addr, l1_pte); + } else if l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return false; + } + + let l2_base = ((l1_pte >> 10) as usize) + .checked_mul(page_size) + .unwrap(); + let l2_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + + let leaf_ppn = phys_start / page_size as u32; + let mut flags = SV32_PTE_V; + if perms.read { + flags |= SV32_PTE_R; + } + if perms.write { + flags |= SV32_PTE_W; + } + if perms.exec { + flags |= SV32_PTE_X; + } + if perms.user { + flags |= types::SV32_PTE_U; + } + let leaf = (leaf_ppn << 10) | flags; + write_pte(l2_addr, leaf); + true +} From e77b9620863fc262ed6a2874e5d617bfa4d36ab6 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 23 Dec 2025 19:45:27 +0200 Subject: [PATCH 24/70] removed un-necessary syscalls --- crates/bootloader/src/syscalls.rs | 150 ++---------------------------- crates/kernel/src/launch.rs | 63 +++---------- crates/vm/src/sys_call.rs | 6 -- 3 files changed, 23 insertions(+), 196 deletions(-) diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index bbe6789..5fbecdb 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -3,17 +3,15 @@ use core::fmt::Write; use std::any::Any; use std::rc::Rc; -use crate::memory::Perms; use state::State; use types::{ADDRESS_LEN, address::Address, result::RESULT_SIZE}; use vm::host_interface::HostInterface; -use vm::memory::{HEAP_PTR_OFFSET, Memory, VirtualAddress, PAGE_SIZE}; +use vm::memory::{HEAP_PTR_OFFSET, Memory, VirtualAddress}; use vm::metering::{MeterResult, Metering}; use vm::registers::Register; use vm::sys_call::{ - SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_COMMIT_STATE, - SYSCALL_CREATE_ACCOUNT, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_MMAP, - SYSCALL_MPROTECT, SYSCALL_MUNMAP, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, + SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, + SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, SyscallHandler, }; @@ -91,11 +89,6 @@ impl SyscallHandler for DefaultSyscallHandler { SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), SYSCALL_TRANSFER => self.sys_transfer(args, memory, host, metering), SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), - SYSCALL_COMMIT_STATE => self.sys_commit_state(args, memory, metering), - SYSCALL_CREATE_ACCOUNT => self.sys_create_account(args, memory, metering), - SYSCALL_MMAP => self.sys_mmap(args, memory, metering), - SYSCALL_MUNMAP => self.sys_munmap(args, memory, metering), - SYSCALL_MPROTECT => self.sys_mprotect(args, memory, metering), SYSCALL_BRK => self.sys_brk(args, memory, metering), _ => { panic!("Unknown syscall: {}", call_id); @@ -809,137 +802,23 @@ impl DefaultSyscallHandler { memory.alloc_on_heap(&bal.to_le_bytes()).as_u32() } - fn sys_commit_state( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let ptr = args[0] as usize; - let len = args[1] as usize; - - if matches!( - metering.on_syscall_data(SYSCALL_COMMIT_STATE, len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_COMMIT_STATE"); - } - - println!("commit state called (ptr=0x{:08x}, len={})", ptr, len); - 0 - } - - fn sys_create_account( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let addr_ptr = args[0] as usize; - let code_ptr = args[1] as usize; - let code_len = args[2] as usize; - - let total_len = ADDRESS_LEN.saturating_add(code_len); - if matches!( - metering.on_syscall_data(SYSCALL_CREATE_ACCOUNT, total_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_CREATE_ACCOUNT"); - } - - let borrowed = memory.as_ref(); - let (addr_start, addr_end) = va_range(addr_ptr, ADDRESS_LEN); - let address_slice = match borrowed.mem_slice(addr_start, addr_end) { - Some(r) => r, - None => return 1, - }; - let mut addr_bytes = [0u8; ADDRESS_LEN]; - addr_bytes.copy_from_slice(address_slice.as_ref()); - let address = Address(addr_bytes); - - let (code_start, code_end) = va_range(code_ptr, code_len); - let code_slice = match borrowed.mem_slice(code_start, code_end) { - Some(r) => r, - None => return 1, - }; - let code = code_slice.to_vec(); - - let mut state = self.state.borrow_mut(); - let account = state.get_account_mut(&address); - account.code = code; - account.is_contract = !account.code.is_empty(); - 0 - } - - // ===== Linux-like memory syscalls (stubs) ===== - fn sys_mmap(&mut self, args: [u32; 6], memory: Memory, _metering: &mut dyn Metering) -> u32 { - // a0=addr (hint), a1=len, a2=prot, a3=flags, a4=fd, a5=offset - let addr_hint = args[0]; - let len = args[1] as usize; - let prot = args[2]; - if len == 0 { - return 0; - } - // Align length to page size - let aligned_len = (len + (PAGE_SIZE - 1)) & !(PAGE_SIZE - 1); - let perms = prot_to_perms(prot); - - // Choose base VA: use hint if provided, else bump next_heap. - let base = if addr_hint != 0 { - VirtualAddress(addr_hint) - } else { - let hint = memory.next_heap(); - VirtualAddress((hint.as_u32() + (PAGE_SIZE as u32 - 1)) & !(PAGE_SIZE as u32 - 1)) - }; - - memory.map_range(base, aligned_len, perms); - if addr_hint == 0 { - let new_break = base - .checked_add(aligned_len as u32) - .unwrap_or(VirtualAddress(0)); - memory.set_next_heap(new_break); - } - base.as_u32() - } - - /// Linux-like `munmap(2)` stub: accepts a VA/len pair and returns success. - /// Unmapping is not implemented yet, so this is a no-op placeholder. - fn sys_munmap( - &mut self, - _args: [u32; 6], - _memory: Memory, - _metering: &mut dyn Metering, - ) -> u32 { - 0 - } - - /// Linux-like `mprotect(2)` stub: ignores requested protections and returns success. - /// Permission changes are not tracked in this minimal MMU. - fn sys_mprotect( - &mut self, - _args: [u32; 6], - _memory: Memory, - _metering: &mut dyn Metering, - ) -> u32 { - 0 - } - /// Minimal `brk(2)` implementation: /// - a0 = new_break; if 0, return current break. /// - Only moves the break forward; shrink requests are ignored. - fn sys_brk(&mut self, _args: [u32; 6], _memory: Memory, _metering: &mut dyn Metering) -> u32 { - let new_brk = _args[0]; - let current = _memory.next_heap().as_u32(); + fn sys_brk(&mut self, args: [u32; 6], memory: Memory, _metering: &mut dyn Metering) -> u32 { + let new_brk = args[0]; + let current = memory.next_heap().as_u32(); if new_brk == 0 { return current; } if new_brk >= current { - _memory.set_next_heap(VirtualAddress(new_brk)); + memory.set_next_heap(VirtualAddress(new_brk)); new_brk } else { current } } + } fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { @@ -947,16 +826,3 @@ fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { let end = start.wrapping_add(len as u32); (start, end) } - -fn prot_to_perms(prot: u32) -> Perms { - // Map PROT_* bits (POSIX) to internal permission flags. - let read = prot & 0x1 != 0; - let write = prot & 0x2 != 0; - let exec = prot & 0x4 != 0; - Perms { - read, - write, - exec, - user: true, - } -} diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index 938531a..9427985 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -53,7 +53,6 @@ const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap pub const PROGRAM_VA_BASE: u32 = 0x0; -const SYSCALL_BRK: u32 = 214; // Location of the page that hosts the satp-switch trampoline. Kept just past // the user window so it does not collide with program text/stack/heap. This VA // is mapped into both roots so the satp write does not invalidate the @@ -112,39 +111,25 @@ pub fn prep_program_task( } // Make sure the kernel page allocator does not hand out frames that the - // kernel heap already consumed (account code lives there). We query the - // current program break via brk(0) and bump the allocator past that PPN. - let heap_brk = current_heap_break(); - if let Some(heap_phys) = mmu::translate_user_va(mmu::current_root(), heap_brk.saturating_sub(1)) { - let min_ppn = ((heap_phys / PAGE_SIZE) as u32).saturating_add(1); + // kernel heap already consumed (account code lives there). We conservatively + // push the allocator toward the top of memory so new user roots/tables + // don't overlap heap-backed data. + let total_ppn = unsafe { + BOOT_INFO + .get_mut() + .as_ref() + .map(|b| b.memory_size / PAGE_SIZE as u32) + .unwrap_or(0) + }; + let reserve = (PROGRAM_WINDOW_BYTES / PAGE_SIZE) as u32 + 4; // user window + a few tables + if total_ppn > reserve { + let min_ppn = total_ppn - reserve; mmu::bump_page_allocator(min_ppn); logf!( - "prep_program_task: bump page alloc to ppn=0x%x from heap_brk=0x%x phys=0x%x", + "prep_program_task: bump page alloc to ppn=0x%x (total_ppn=0x%x)", min_ppn, - heap_brk, - heap_phys as u32 + total_ppn ); - } else { - // Fallback: push the allocator toward the top of memory so we avoid heap overlap. - let total_ppn = unsafe { - BOOT_INFO - .get_mut() - .as_ref() - .map(|b| b.memory_size / PAGE_SIZE as u32) - .unwrap_or(0) - }; - let reserve = (PROGRAM_WINDOW_BYTES / PAGE_SIZE) as u32 + 4; // user window + a few tables - if total_ppn > reserve { - let min_ppn = total_ppn - reserve; - mmu::bump_page_allocator(min_ppn); - logf!( - "prep_program_task: fallback bump page alloc to ppn=0x%x (total_ppn=0x%x)", - min_ppn, - total_ppn - ); - } else { - logf!("prep_program_task: could not translate heap_brk=0x%x", heap_brk); - } } let asid = alloc_asid(); @@ -403,21 +388,3 @@ fn alloc_asid() -> u16 { asid } } - -/// Query the current heap break via the `brk(2)` syscall (a7 = 214). -fn current_heap_break() -> u32 { - let mut brk: u32; - unsafe { - core::arch::asm!( - "mv a0, zero", - "li a7, {sys_brk}", - "ecall", - out("a0") brk, - sys_brk = const SYSCALL_BRK, - out("a1") _, out("a2") _, out("a3") _, out("a4") _, out("a5") _, out("a6") _, - out("t0") _, out("t1") _, out("t2") _, - options(nostack) - ); - } - brk -} diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index 8acc01f..3309381 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -14,12 +14,6 @@ pub const SYSCALL_ALLOC: u32 = 7; pub const SYSCALL_DEALLOC: u32 = 8; pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; -pub const SYSCALL_COMMIT_STATE: u32 = 11; -pub const SYSCALL_CREATE_ACCOUNT: u32 = 12; -// Linux/RISC-V memory management syscall numbers: -pub const SYSCALL_MMAP: u32 = 222; // mmap(2): map pages with PROT/FLAGS -pub const SYSCALL_MUNMAP: u32 = 215; // munmap(2): unmap a VA range -pub const SYSCALL_MPROTECT: u32 = 226; // mprotect(2): change page protections pub const SYSCALL_BRK: u32 = 214; // brk(2): set program break (heap end) /// Trait implemented by syscall handlers consumed by the VM. From 62bf1bd2164553cce2e8b986288483ce725e7b39 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 23 Dec 2025 23:45:21 +0200 Subject: [PATCH 25/70] Refactor kernel transaction handling into modules --- crates/kernel/src/bundle.rs | 34 ++++++ crates/kernel/src/create_account.rs | 38 +++++++ crates/kernel/src/main.rs | 159 ++-------------------------- crates/kernel/src/program_call.rs | 98 +++++++++++++++++ 4 files changed, 176 insertions(+), 153 deletions(-) create mode 100644 crates/kernel/src/bundle.rs create mode 100644 crates/kernel/src/create_account.rs create mode 100644 crates/kernel/src/program_call.rs diff --git a/crates/kernel/src/bundle.rs b/crates/kernel/src/bundle.rs new file mode 100644 index 0000000..34aac24 --- /dev/null +++ b/crates/kernel/src/bundle.rs @@ -0,0 +1,34 @@ +use core::mem::forget; + +use program::{log, logf}; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; + +use crate::create_account::create_account; +use crate::program_call::program_call; + +pub(crate) fn process_bundle(encoded_bundle: &[u8]) { + if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { + let count = bundle.transactions.len(); + logf!("decoded tx count=%d", count as u32); + for i in 0..count { + logf!("processing tx %d/%d", (i + 1) as u32, count as u32); + if let Some(tx) = bundle.transactions.get(i) { + execute_transaction(tx); + } else { + logf!("missing tx at index %d", i as u32); + } + } + // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. + forget(bundle); + } else { + log!("bundle decode failed"); + } +} + +fn execute_transaction(tx: &Transaction) { + match tx.tx_type { + TransactionType::CreateAccount => create_account(tx), + TransactionType::ProgramCall => program_call(tx), + _ => log!("executing transaction"), + } +} diff --git a/crates/kernel/src/create_account.rs b/crates/kernel/src/create_account.rs new file mode 100644 index 0000000..77a97b2 --- /dev/null +++ b/crates/kernel/src/create_account.rs @@ -0,0 +1,38 @@ +use alloc::format; + +use kernel::global::STATE; +use kernel::Config; +use program::log; +use state::State; +use types::transaction::Transaction; + +pub(crate) fn create_account(tx: &Transaction) { + let code_size = tx.data.len(); + let is_contract = code_size > 0; + + let msg = format!( + "Tx creating account at address {}. Is contract: {}. Code size: {} bytes.", + tx.to, is_contract, code_size + ); + let msg_ref: &str = msg.as_str(); + log!(msg_ref); + + let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + if code_size > max { + panic!( + "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", + code_size, max + ); + } + + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = state.get_account_mut(&tx.to); + account.code = tx.data.clone(); + account.is_contract = is_contract; + let msg = format!( + "account created in kernel state: addr={} is_contract={} code_len={}", + tx.to, is_contract, code_size + ); + let msg_ref: &str = msg.as_str(); + log!(msg_ref); +} diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index bc7ae78..44d0a54 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -3,16 +3,15 @@ extern crate alloc; -use alloc::{format, vec}; -use core::mem::forget; use core::slice; -use kernel::{BootInfo, Config, prep_program_task, run_task, PROGRAM_WINDOW_BYTES}; +use kernel::BootInfo; use program::{log, logf}; -use state::State; -use types::transaction::{Transaction, TransactionBundle, TransactionType}; mod init; -use kernel::global::{BOOT_INFO, STATE, TASKS}; +mod bundle; +mod create_account; +mod program_call; +use crate::bundle::process_bundle; use crate::init::init_kernel; #[allow(dead_code)] @@ -36,158 +35,12 @@ pub extern "C" fn _start( init_kernel(state_ptr, state_len, boot_info_ptr); let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; - - if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { - let count = bundle.transactions.len(); - logf!("decoded tx count=%d", count as u32); - for i in 0..count { - logf!("processing tx %d/%d", (i + 1) as u32, count as u32); - if let Some(tx) = bundle.transactions.get(i) { - execute_transaction(tx); - } else { - logf!("missing tx at index %d", i as u32); - } - } - // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. - forget(bundle); - } else { - log!("bundle decode failed"); - } + process_bundle(encoded_bundle); log!("finished bundle execution"); halt(); } -fn execute_transaction(tx: &Transaction) { - match tx.tx_type { - TransactionType::CreateAccount => create_account(tx), - TransactionType::ProgramCall => program_call(tx), - _ => log!("executing transaction"), - } -} - -fn create_account(tx: &Transaction) { - let code_size = tx.data.len(); - let is_contract = code_size > 0; - - let msg = format!( - "Tx creating account at address {}. Is contract: {}. Code size: {} bytes.", - tx.to, is_contract, code_size - ); - let msg_ref: &str = msg.as_str(); - log!(msg_ref); - - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; - if code_size > max { - panic!( - "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", - code_size, max - ); - } - - let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; - let account = state.get_account_mut(&tx.to); - account.code = tx.data.clone(); - account.is_contract = is_contract; - let msg = format!( - "account created in kernel state: addr={} is_contract={} code_len={}", - tx.to, is_contract, code_size - ); - let msg_ref: &str = msg.as_str(); - log!(msg_ref); -} - -fn program_call(tx: &Transaction) { - let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; - let account = match state.get_account(&tx.to) { - Some(acc) => acc, - None => { - logf!( - "%s", - display: format!("Program call failed: account {} does not exist", tx.to) - ); - return; - } - }; - - if !account.is_contract { - logf!( - "%s", - display: format!( - "Program call failed: target {} is not a contract (code_len={})", - tx.to, - account.code.len() - ) - ); - return; - } - - let first_nz = account - .code - .iter() - .position(|&b| b != 0) - .unwrap_or(account.code.len()); - let nz_count = account.code.iter().filter(|&&b| b != 0).count(); - logf!( - "%s", - display: format!( - "Program code stats: len={} first_nz={} nz_count={}", - account.code.len(), - first_nz, - nz_count - ) - ); - - let code_len = account.code.len(); - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; - if code_len > max { - panic!( - "❌ Program call rejected: code size ({}) exceeds limit ({})", - code_len, max - ); - } - - logf!( - "%s", - display: format!( - "Program call: from={} to={} input_len={} code_len={}", - tx.from, - tx.to, - tx.data.len(), - code_len - ) - ); - - let kstack_top = unsafe { BOOT_INFO.get_mut().as_ref().map(|b| b.kstack_top).unwrap_or(0) }; - - let entry_off = first_nz as u32; - - if let Some(task) = - prep_program_task(kstack_top, &tx.to, &tx.from, &account.code, &tx.data, entry_off) - { - logf!( - "Program task created: root=0x%x asid=%d window_size=%d", - task.addr_space.root_ppn, - task.addr_space.asid as u32, - PROGRAM_WINDOW_BYTES as u32 - ); - unsafe { - let tasks_slot = TASKS.get_mut(); - match tasks_slot { - Some(tasks) => tasks.push(task), - None => *tasks_slot = Some(vec![task]), - } - if let Some(tasks) = tasks_slot { - if let Some(last) = tasks.last() { - run_task(last); - } - } - } - } else { - log!("Program call skipped: no memory manager installed"); - } -} - #[inline(never)] fn halt() -> ! { unsafe { core::arch::asm!("ebreak") }; diff --git a/crates/kernel/src/program_call.rs b/crates/kernel/src/program_call.rs new file mode 100644 index 0000000..5ca9dde --- /dev/null +++ b/crates/kernel/src/program_call.rs @@ -0,0 +1,98 @@ +use alloc::{format, vec}; + +use kernel::{prep_program_task, run_task, Config, PROGRAM_WINDOW_BYTES}; +use kernel::global::{BOOT_INFO, STATE, TASKS}; +use program::{log, logf}; +use state::State; +use types::transaction::Transaction; + +pub(crate) fn program_call(tx: &Transaction) { + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = match state.get_account(&tx.to) { + Some(acc) => acc, + None => { + logf!( + "%s", + display: format!("Program call failed: account {} does not exist", tx.to) + ); + return; + } + }; + + if !account.is_contract { + logf!( + "%s", + display: format!( + "Program call failed: target {} is not a contract (code_len={})", + tx.to, + account.code.len() + ) + ); + return; + } + + let first_nz = account + .code + .iter() + .position(|&b| b != 0) + .unwrap_or(account.code.len()); + let nz_count = account.code.iter().filter(|&&b| b != 0).count(); + logf!( + "%s", + display: format!( + "Program code stats: len={} first_nz={} nz_count={}", + account.code.len(), + first_nz, + nz_count + ) + ); + + let code_len = account.code.len(); + let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + if code_len > max { + panic!( + "❌ Program call rejected: code size ({}) exceeds limit ({})", + code_len, max + ); + } + + logf!( + "%s", + display: format!( + "Program call: from={} to={} input_len={} code_len={}", + tx.from, + tx.to, + tx.data.len(), + code_len + ) + ); + + let kstack_top = unsafe { BOOT_INFO.get_mut().as_ref().map(|b| b.kstack_top).unwrap_or(0) }; + + let entry_off = first_nz as u32; + + if let Some(task) = + prep_program_task(kstack_top, &tx.to, &tx.from, &account.code, &tx.data, entry_off) + { + logf!( + "Program task created: root=0x%x asid=%d window_size=%d", + task.addr_space.root_ppn, + task.addr_space.asid as u32, + PROGRAM_WINDOW_BYTES as u32 + ); + unsafe { + let tasks_slot = TASKS.get_mut(); + match tasks_slot { + Some(tasks) => tasks.push(task), + None => *tasks_slot = Some(vec![task]), + } + if let Some(tasks) = tasks_slot { + if let Some(last) = tasks.last() { + run_task(last); + } + } + } + } else { + log!("Program call skipped: no memory manager installed"); + } +} From 401abffe8bb01554efb741ad044e449913b5d4fc Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Thu, 25 Dec 2025 11:04:37 +0200 Subject: [PATCH 26/70] WIP trap --- crates/kernel/src/init.rs | 14 +++- crates/kernel/src/lib.rs | 3 + crates/kernel/src/main.rs | 1 - crates/kernel/src/syscall.rs | 91 +++++++++++++++++++++++ crates/kernel/src/trap.rs | 137 +++++++++++++++++++++++++++++++++++ crates/program/src/log.rs | 4 +- crates/vm/src/cpu.rs | 55 +++++++++++++- crates/vm/src/decoder.rs | 2 +- crates/vm/src/exe.rs | 25 ++++++- crates/vm/src/sys_call.rs | 2 +- 10 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 crates/kernel/src/syscall.rs create mode 100644 crates/kernel/src/trap.rs diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index 6c42658..bc7e4cf 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -5,15 +5,23 @@ use program::{log, logf}; use state::State; use kernel::global::{BOOT_INFO, PAGE_ALLOC_INIT, STATE, TASKS}; -use kernel::{mmu, BootInfo, Task}; +use kernel::{mmu, BootInfo, Task, trap}; /// Initialize kernel state from the bootloader handoff and optional state blob. pub fn init_kernel(state_ptr: *const u8, state_len: usize, boot_info_ptr: *const BootInfo) { + if let Some(info) = unsafe { boot_info_ptr.as_ref() } { + trap::init_trap_vector(info.kstack_top); + } init_state(state_ptr, state_len); init_boot_info(boot_info_ptr); } fn init_state(state_ptr: *const u8, state_len: usize) { + logf!( + "init_state: state_ptr=0x%x state_len=%d", + state_ptr as usize as u32, + state_len as u32 + ); unsafe { let state_slot = STATE.get_mut(); if !state_ptr.is_null() && state_len > 0 { @@ -32,6 +40,10 @@ fn init_state(state_ptr: *const u8, state_len: usize) { } fn init_boot_info(boot_info_ptr: *const BootInfo) { + logf!( + "init_boot_info: boot_info_ptr=0x%x", + boot_info_ptr as usize as u32 + ); if let Some(info) = unsafe { boot_info_ptr.as_ref() } { unsafe { *BOOT_INFO.get_mut() = Some(*info); diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 09b68fe..61bd32e 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -1,4 +1,5 @@ #![no_std] +#![feature(naked_functions)] pub mod config; pub use config::Config; @@ -9,3 +10,5 @@ pub use task::{AddressSpace, Task, TrapFrame}; pub mod launch; pub use launch::{prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; pub mod mmu; +pub mod trap; +pub mod syscall; diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 44d0a54..7946227 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -30,7 +30,6 @@ pub extern "C" fn _start( boot_info_ptr: *const BootInfo, ) { log!("kernel boot"); - logf!("bundle_len=%d", bundle_len as u32); init_kernel(state_ptr, state_len, boot_info_ptr); diff --git a/crates/kernel/src/syscall.rs b/crates/kernel/src/syscall.rs new file mode 100644 index 0000000..a5da543 --- /dev/null +++ b/crates/kernel/src/syscall.rs @@ -0,0 +1,91 @@ +//! Kernel-owned syscall stubs. These mirror the bootloader syscalls but +//! are now dispatched from the kernel trap handler. Implementations will +//! land here; for now they panic to make missing pieces explicit. +use program::{log, logf}; + +pub const SYSCALL_STORAGE_GET: u32 = 1; +pub const SYSCALL_STORAGE_SET: u32 = 2; +pub const SYSCALL_PANIC: u32 = 3; +pub const SYSCALL_LOG: u32 = 100; +pub const SYSCALL_CALL_PROGRAM: u32 = 5; +pub const SYSCALL_FIRE_EVENT: u32 = 6; +pub const SYSCALL_ALLOC: u32 = 7; +pub const SYSCALL_DEALLOC: u32 = 8; +pub const SYSCALL_TRANSFER: u32 = 9; +pub const SYSCALL_BALANCE: u32 = 10; +pub const SYSCALL_BRK: u32 = 214; + +pub fn dispatch_syscall(call_id: u32, args: [u32; 6]) -> u32 { + match call_id { + SYSCALL_STORAGE_GET => sys_storage_get(args), + SYSCALL_STORAGE_SET => sys_storage_set(args), + SYSCALL_PANIC => sys_panic(args), + SYSCALL_LOG => sys_log(args), + SYSCALL_CALL_PROGRAM => sys_call_program(args), + SYSCALL_FIRE_EVENT => sys_fire_event(args), + SYSCALL_ALLOC => sys_alloc(args), + SYSCALL_DEALLOC => sys_dealloc(args), + SYSCALL_TRANSFER => sys_transfer(args), + SYSCALL_BALANCE => sys_balance(args), + SYSCALL_BRK => sys_brk(args), + _ => { + logf!("unknown syscall id %d", call_id); + 0 + } + } +} + +fn sys_storage_get(_args: [u32; 6]) -> u32 { + log!("sys_storage_get: need implementation"); + 0 +} + +fn sys_storage_set(_args: [u32; 6]) -> u32 { + log!("sys_storage_set: need implementation"); + 0 +} + +fn sys_panic(_args: [u32; 6]) -> u32 { + log!("sys_panic: need implementation"); + 0 +} + +fn sys_log(_args: [u32; 6]) -> u32 { + log!("sys_log: need implementation"); + 0 +} + +fn sys_call_program(_args: [u32; 6]) -> u32 { + log!("sys_call_program: need implementation"); + 0 +} + +fn sys_fire_event(_args: [u32; 6]) -> u32 { + log!("sys_fire_event: need implementation"); + 0 +} + +fn sys_alloc(_args: [u32; 6]) -> u32 { + log!("sys_alloc: need implementation"); + 0 +} + +fn sys_dealloc(_args: [u32; 6]) -> u32 { + log!("sys_dealloc: need implementation"); + 0 +} + +fn sys_transfer(_args: [u32; 6]) -> u32 { + log!("sys_transfer: need implementation"); + 0 +} + +fn sys_balance(_args: [u32; 6]) -> u32 { + log!("sys_balance: need implementation"); + 0 +} + +fn sys_brk(_args: [u32; 6]) -> u32 { + log!("sys_brk: need implementation"); + 0 +} diff --git a/crates/kernel/src/trap.rs b/crates/kernel/src/trap.rs new file mode 100644 index 0000000..2d68763 --- /dev/null +++ b/crates/kernel/src/trap.rs @@ -0,0 +1,137 @@ +use core::arch::asm; +use program::{log, logf}; + +use crate::syscall; + +const SCAUSE_ECALL_FROM_U: usize = 8; +const SAVED_REG_COUNT: usize = 11; + +/// Install the kernel trap vector and set up the kernel stack for traps. +pub fn init_trap_vector(kstack_top: u32) { + logf!("init_trap_vector: kstack_top=0x%x", kstack_top); + unsafe { + asm!("csrw sscratch, {0}", in(reg) kstack_top); + asm!("csrw stvec, {0}", in(reg) trap_entry as usize); + } +} + +/// Trap entry stub: +/// - Switch to the kernel stack via sscratch. +/// - Save sepc, ra, a0-a7, and t0. +/// - Call into the Rust trap handler with a pointer to the saved area. +/// - Restore registers and return with sret. +// #[unsafe(naked)] +pub unsafe extern "C" fn trap_entry() -> ! { + unsafe { + asm!( + // Switch to kernel stack and make room for saved registers. + "csrrw sp, sscratch, sp", + "addi sp, sp, -44", + // Save caller-saved registers we clobber and sepc. + "sw t0, 40(sp)", + "csrr t0, sepc", + "sw t0, 0(sp)", // saved sepc + "sw ra, 4(sp)", + "sw a0, 8(sp)", + "sw a1, 12(sp)", + "sw a2, 16(sp)", + "sw a3, 20(sp)", + "sw a4, 24(sp)", + "sw a5, 28(sp)", + "sw a6, 32(sp)", + "sw a7, 36(sp)", + // Call Rust trap handler with pointer to the save area in a0. + "mv a0, sp", + "call {handler}", + // Restore sepc and registers, then return from trap. + "lw t0, 0(sp)", + "csrw sepc, t0", + "lw ra, 4(sp)", + "lw a0, 8(sp)", + "lw a1, 12(sp)", + "lw a2, 16(sp)", + "lw a3, 20(sp)", + "lw a4, 24(sp)", + "lw a5, 28(sp)", + "lw a6, 32(sp)", + "lw a7, 36(sp)", + "lw t0, 40(sp)", + "addi sp, sp, 44", + "csrrw sp, sscratch, sp", + "sret", + handler = sym handle_trap + ); + core::hint::unreachable_unchecked(); + } +} + +/// Rust-level trap handler. Receives a pointer to the saved register block +/// laid out as: +/// [0] sepc, [1] ra, [2] a0, [3] a1, [4] a2, [5] a3, [6] a4, [7] a5, +/// [8] a6, [9] a7, [10] t0. +#[unsafe(no_mangle)] +pub extern "C" fn handle_trap(saved: *mut u32) { + let regs = unsafe { core::slice::from_raw_parts_mut(saved, SAVED_REG_COUNT) }; + let scause = read_scause(); + let stval = read_stval(); + let sepc = regs[0]; + let satp = read_satp(); + let sp: u32; + unsafe { asm!("mv {0}, sp", out(reg) sp); } + + // logf!( + // "trap_entry: scause=0x%x stval=0x%x sepc=0x%x satp=0x%x sp=0x%x", + // scause as u32, + // stval as u32, + // sepc, + // satp, + // sp + // ); + let is_interrupt = (scause >> 31) != 0; + if is_interrupt { + panic!( + "unexpected interrupt trap: scause=0x{:x} stval=0x{:x} sepc=0x{:08x}", + scause, stval, sepc + ); + } + + let code = scause & 0xfff; + match code { + SCAUSE_ECALL_FROM_U => { + let args = [ + regs[3], // a1 + regs[4], // a2 + regs[5], // a3 + regs[6], // a4 + regs[7], // a5 + regs[8], // a6 + ]; + let call_id = regs[9]; // a7 + let ret = syscall::dispatch_syscall(call_id, args); + regs[2] = ret; // a0 return value + regs[0] = regs[0].wrapping_add(4); // Advance past ecall + } + _ => log!("unhandled trap"), + } +} + +#[inline(always)] +fn read_scause() -> usize { + let value: usize; + unsafe { asm!("csrr {0}, scause", out(reg) value); } + value +} + +#[inline(always)] +fn read_satp() -> u32 { + let value: u32; + unsafe { asm!("csrr {0}, satp", out(reg) value); } + value +} + +#[inline(always)] +fn read_stval() -> usize { + let value: usize; + unsafe { asm!("csrr {0}, stval", out(reg) value); } + value +} diff --git a/crates/program/src/log.rs b/crates/program/src/log.rs index 01da956..16c4978 100644 --- a/crates/program/src/log.rs +++ b/crates/program/src/log.rs @@ -4,7 +4,7 @@ macro_rules! logf_syscall { #[cfg(target_arch = "riscv32")] unsafe { core::arch::asm!( - "li a7, 4", // syscall_log + "li a7, 100", // syscall_log "ecall", in("a1") $fmt_ptr, in("a2") $fmt_len, @@ -201,4 +201,4 @@ impl<'a> core::fmt::Write for BufferWriter<'a> { Ok(()) } } -} \ No newline at end of file +} diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 0ba0562..0415630 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -12,6 +12,11 @@ use std::rc::Rc; mod exec; pub const CSR_SATP: u16 = 0x180; +pub const CSR_STVEC: u16 = 0x105; +pub const CSR_SEPC: u16 = 0x141; +pub const CSR_SCAUSE: u16 = 0x142; +pub const CSR_STVAL: u16 = 0x143; +const SCAUSE_ECALL_FROM_U: u32 = 8; /// Represents the Central Processing Unit (CPU) of our RISC-V virtual machine. /// @@ -182,6 +187,44 @@ impl CPU { true } + fn trap_to_vector(&mut self, cause: u32, trap_value: u32, syscall_id: Option) -> bool { + if !self.write_csr(CSR_SEPC, self.pc) { + panic!("trap_to_vector: failed to write sepc"); + } + if !self.write_csr(CSR_SCAUSE, cause) { + panic!("trap_to_vector: failed to write scause"); + } + if !self.write_csr(CSR_STVAL, trap_value) { + panic!("trap_to_vector: failed to write stval"); + } + let stvec = match self.read_csr(CSR_STVEC) { + Some(val) => val & !0x3, + None => return false, + }; + if let Some(syscall_id) = syscall_id { + self.log( + &format!( + "trap_to_vector: pc=0x{:08x} -> stvec=0x{:08x} cause=0x{:x} stval=0x{:x} syscall=0x{:x}", + self.pc, stvec, cause, trap_value, syscall_id + ), + false, + ); + } else { + self.log( + &format!( + "trap_to_vector: pc=0x{:08x} -> stvec=0x{:08x} cause=0x{:x} stval=0x{:x}", + self.pc, stvec, cause, trap_value + ), + false, + ); + } + self.set_pc(stvec) + } + + fn has_trap_vector(&self) -> bool { + self.csrs.contains_key(&CSR_STVEC) + } + /// Executes a single instruction cycle (fetch, decode, execute). /// /// EDUCATIONAL PURPOSE: This is the heart of the CPU - the instruction cycle. @@ -282,7 +325,17 @@ impl CPU { let old_pc = self.pc; // EDUCATIONAL: Execute the instruction - let result = self.execute(instr, memory, host); + let result = self.execute(instr.clone(), memory, host); + if !result { + self.log( + &format!( + "Execution halted at PC=0x{:08x} on instr={}", + self.pc, + instr.pretty_print() + ), + false, + ); + } // EDUCATIONAL: Only increment PC if the instruction didn't change it // This handles branches, jumps, and calls correctly diff --git a/crates/vm/src/decoder.rs b/crates/vm/src/decoder.rs index 9c901d9..31f7aa5 100644 --- a/crates/vm/src/decoder.rs +++ b/crates/vm/src/decoder.rs @@ -411,7 +411,7 @@ pub fn decode_full(word: u32) -> Option { match funct12 { 0 => Some(Instruction::Ecall), 1 => Some(Instruction::Ebreak), - 0x302 => Some(Instruction::Mret), + 0x302 | 0x102 => Some(Instruction::Mret), _ => None, } } diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index afa56d2..d483f2e 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,4 +1,5 @@ -use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP}; +use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP, CSR_SEPC, SCAUSE_ECALL_FROM_U}; +use crate::sys_call::SYSCALL_LOG; use crate::memory::VirtualAddress; use crate::host_interface::HostInterface; use crate::instruction::CsrOp; @@ -792,6 +793,18 @@ impl CPU { Some(v) => v, None => return false, }; + if self.has_trap_vector() { + // Bypass trap for logging syscalls so they execute directly. + if call_id != SYSCALL_LOG { + if !self.trap_to_vector(SCAUSE_ECALL_FROM_U, 0, Some(call_id)) { + panic!( + "trap_to_vector returned false for ecall id={} pc=0x{:08x}", + call_id, self.pc + ); + } + return true; + } + } let (result, cont) = self.syscall_handler.handle_syscall( call_id, args, @@ -865,8 +878,14 @@ impl CPU { return false; } Instruction::Mret => { - // Treat MRET as a simple return/halt in this VM - return false; + let target = match self.read_csr(CSR_SEPC) { + Some(v) => v, + None => return false, + }; + if !self.set_pc(target) { + return false; + } + return true; } // EDUCATIONAL: Compressed instruction set (RV32C) - space-saving instructions diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index 3309381..01064c0 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -7,7 +7,7 @@ use core::any::Any; pub const SYSCALL_STORAGE_GET: u32 = 1; pub const SYSCALL_STORAGE_SET: u32 = 2; pub const SYSCALL_PANIC: u32 = 3; -pub const SYSCALL_LOG: u32 = 4; +pub const SYSCALL_LOG: u32 = 100; pub const SYSCALL_CALL_PROGRAM: u32 = 5; pub const SYSCALL_FIRE_EVENT: u32 = 6; pub const SYSCALL_ALLOC: u32 = 7; From d74de33c05ccc3b9a85698bb20a5f92e8ecf098a Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 09:21:21 +0200 Subject: [PATCH 27/70] Refactor memory: move Sv32 MMU into vm, expose API-only memory access, and update bootloader --- crates/bootloader/src/bootloader.rs | 10 ++--- crates/bootloader/src/lib.rs | 5 +-- crates/bootloader/src/memory/mod.rs | 8 ---- crates/bootloader/src/memory/pte.rs | 33 ---------------- crates/bootloader/tests/allocator_test.rs | 11 +++--- crates/vm/src/cpu.rs | 5 +++ crates/vm/src/exe.rs | 9 +++-- crates/vm/src/{memory.rs => memory/mod.rs} | 16 +++++++- .../memory.rs => vm/src/memory/sv32.rs} | 38 ++++++++++--------- crates/vm/src/vm.rs | 5 ++- 10 files changed, 59 insertions(+), 81 deletions(-) delete mode 100644 crates/bootloader/src/memory/mod.rs delete mode 100644 crates/bootloader/src/memory/pte.rs rename crates/vm/src/{memory.rs => memory/mod.rs} (94%) rename crates/{bootloader/src/memory/memory.rs => vm/src/memory/sv32.rs} (97%) diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 88bbd16..aa17c67 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -9,10 +9,9 @@ use goblin::elf::Elf; use types::{boot::BootInfo, transaction::TransactionBundle, SV32_DIRECT_MAP_BASE}; use crate::DefaultSyscallHandler; -use crate::memory::{Memory, Perms}; use state::State; use vm::host_interface::NoopHost; -use vm::memory::{Mmu, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; +use vm::memory::{API, Mmu, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; use vm::registers::Register; use vm::vm::VM; @@ -38,14 +37,14 @@ impl Default for BootConfig { #[derive(Debug)] pub struct Bootloader { pub config: BootConfig, - memory: Rc, + memory: Rc, } impl Bootloader { pub fn new(total_size_bytes: usize) -> Self { Self { config: BootConfig::default(), - memory: Rc::new(Memory::new(total_size_bytes, PAGE_SIZE)), + memory: Rc::new(Sv32Memory::new(total_size_bytes, PAGE_SIZE)), } } @@ -105,7 +104,8 @@ impl Bootloader { Perms::rw_kernel(), ); assert!(mapped, "failed to map kernel direct physical window"); - (entry_point, self.memory.clone() as MmuRef) + let memory: MmuRef = self.memory.clone(); + (entry_point, memory) } /// Execute a transaction bundle by delegating to the kernel. This mirrors the diff --git a/crates/bootloader/src/lib.rs b/crates/bootloader/src/lib.rs index c61d65f..d73296f 100644 --- a/crates/bootloader/src/lib.rs +++ b/crates/bootloader/src/lib.rs @@ -5,13 +5,10 @@ //! - loads a kernel program into fresh pages, //! - hands the loaded image off to a future kernel runtime. //! -//! Memory utilities are local copies of the VM page primitives to keep the OS -//! independent from the execution engine. +//! Memory utilities are provided by the VM crate. pub mod bootloader; -pub mod memory; - pub mod syscalls; pub use vm::sys_call; diff --git a/crates/bootloader/src/memory/mod.rs b/crates/bootloader/src/memory/mod.rs deleted file mode 100644 index 3bb92bd..0000000 --- a/crates/bootloader/src/memory/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Simple in-memory pages for the OS boot/runtime layers. - -mod memory; -mod pte; - -pub use memory::Memory; -pub use vm::memory::Perms; -pub(crate) use pte::Pte; diff --git a/crates/bootloader/src/memory/pte.rs b/crates/bootloader/src/memory/pte.rs deleted file mode 100644 index edca6bd..0000000 --- a/crates/bootloader/src/memory/pte.rs +++ /dev/null @@ -1,33 +0,0 @@ -/// Simple Sv32-style page table entry used by the software MMU. -/// -/// Layout mirrors the RISC-V Sv32 PTE fields: -/// - `V` (valid) gate keeps the entry. -/// - `R/W/X` control read/write/execute access. -/// - `U` marks user visibility (we keep `G/A/D` out for now). -/// - `PPN` holds the physical page number. -/// -/// For non-leaf entries, `next_l2` indexes an L2 table; for leaf entries, -/// `ppn` points at the mapped frame. -#[derive(Clone, Copy, Debug, Default)] -pub struct Pte { - /// Valid bit: entry is present. - pub valid: bool, - /// Read permission. - pub read: bool, - /// Write permission. - pub write: bool, - /// Execute permission. - pub exec: bool, - /// User visibility (false = supervisor/kernel only). - pub user: bool, - /// Physical page number for a leaf mapping. - pub ppn: usize, - /// Index of next-level L2 table for non-leaf entries. - pub next_l2: Option, -} - -impl Pte { - pub fn is_leaf(&self) -> bool { - self.valid && (self.read || self.write || self.exec) - } -} diff --git a/crates/bootloader/tests/allocator_test.rs b/crates/bootloader/tests/allocator_test.rs index dcd50f5..b3531c6 100644 --- a/crates/bootloader/tests/allocator_test.rs +++ b/crates/bootloader/tests/allocator_test.rs @@ -1,16 +1,15 @@ -use bootloader::memory::Memory as BootMemory; use bootloader::DefaultSyscallHandler; use state::State; use std::cell::RefCell; use std::rc::Rc; use vm::host_interface; -use vm::memory::{Memory, PAGE_SIZE}; +use vm::memory::{Memory, Sv32Memory, PAGE_SIZE}; use vm::metering::NoopMeter; use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { - let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); + let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); @@ -46,7 +45,7 @@ fn test_allocator_syscalls() { #[test] fn test_multiple_allocations() { - let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); + let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); @@ -88,7 +87,7 @@ fn test_multiple_allocations() { #[test] fn test_alignment_requirements() { - let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); + let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); @@ -116,7 +115,7 @@ fn test_alignment_requirements() { #[test] fn test_invalid_alignment() { - let memory: Memory = Rc::new(BootMemory::new(8192, PAGE_SIZE)); + let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 0415630..91b51d0 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -187,6 +187,11 @@ impl CPU { true } + pub fn set_satp(&mut self, memory: &Memory, value: u32) -> bool { + memory.set_satp(value); + self.write_csr(CSR_SATP, value) + } + fn trap_to_vector(&mut self, cause: u32, trap_value: u32, syscall_id: Option) -> bool { if !self.write_csr(CSR_SEPC, self.pc) { panic!("trap_to_vector: failed to write sepc"); diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index d483f2e..b800d4e 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -861,10 +861,11 @@ impl CPU { let will_write = src != 0 || matches!(op, CsrOp::Csrrw); if will_write { if csr == CSR_SATP { - memory.set_satp(new_val); - } - if !self.write_csr(csr, new_val) { - return false; + if !self.set_satp(&memory, new_val) { + panic!("failed to update satp"); + } + } else if !self.write_csr(csr, new_val) { + panic!("failed to write csr 0x{:03x}", csr); } } diff --git a/crates/vm/src/memory.rs b/crates/vm/src/memory/mod.rs similarity index 94% rename from crates/vm/src/memory.rs rename to crates/vm/src/memory/mod.rs index 3b8030c..aacf7b3 100644 --- a/crates/vm/src/memory.rs +++ b/crates/vm/src/memory/mod.rs @@ -1,6 +1,11 @@ use std::cell::Ref; use std::rc::Rc; -use crate::metering::{Metering, MemoryAccessKind}; + +use crate::metering::{MemoryAccessKind, Metering}; + +mod sv32; + +pub use sv32::Sv32Memory; pub const HEAP_PTR_OFFSET: u32 = 0x100; @@ -93,7 +98,7 @@ impl From for usize { } } -pub trait Mmu: std::fmt::Debug { +pub trait MMU: std::fmt::Debug { // --- CPU-facing data access (loads/stores/fetches) --- fn mem(&self) -> Ref>; fn mem_slice(&self, start: VirtualAddress, end: VirtualAddress) -> Option>; @@ -104,6 +109,9 @@ pub trait Mmu: std::fmt::Debug { fn load_byte(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_halfword(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; fn load_word(&self, addr: VirtualAddress, metering: &mut dyn Metering, kind: MemoryAccessKind) -> Option; +} + +pub trait API: std::fmt::Debug { fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms); /// Get the current page-table root (index/identifier). fn current_root(&self) -> usize; @@ -120,4 +128,8 @@ pub trait Mmu: std::fmt::Debug { fn set_next_heap(&self, next: VirtualAddress); } +pub trait Mmu: MMU + API {} + +impl Mmu for T {} + pub type Memory = Rc; diff --git a/crates/bootloader/src/memory/memory.rs b/crates/vm/src/memory/sv32.rs similarity index 97% rename from crates/bootloader/src/memory/memory.rs rename to crates/vm/src/memory/sv32.rs index fef734a..6292e9b 100644 --- a/crates/bootloader/src/memory/memory.rs +++ b/crates/vm/src/memory/sv32.rs @@ -3,11 +3,13 @@ use std::collections::HashMap; use std::rc::Rc; use types::{ - Sv32PagePerms, Sv32PageTable, SV32_PTE_R, SV32_PTE_V, SV32_PTE_W, SV32_PTE_X, - SV32_SATP_PPN_MASK, map_allocating, map_to_physical, + map_allocating, map_to_physical, Sv32PagePerms, Sv32PageTable, SV32_PTE_R, SV32_PTE_V, + SV32_PTE_W, SV32_PTE_X, SV32_SATP_PPN_MASK, }; -use vm::memory::{Mmu, Perms, VirtualAddress}; -use vm::metering::{MeterResult, Metering, MemoryAccessKind}; + +use crate::metering::{MemoryAccessKind, MeterResult, Metering}; + +use super::{API, MMU, Perms, VirtualAddress}; /// Software Sv32 MMU backed by a contiguous physical buffer. /// @@ -27,7 +29,7 @@ use vm::metering::{MeterResult, Metering, MemoryAccessKind}; /// - `mem_slice` only returns contiguous slices when the mapped physical pages are contiguous. /// - Identity mapping is not assumed; everything uses page tables even for kernel. #[derive(Debug)] -pub struct Memory { +pub struct Sv32Memory { /// Page size in bytes (Sv32: 4 KiB). page_size: usize, /// Total number of physical frames available. @@ -51,7 +53,7 @@ fn perms_to_sv32(perms: Perms) -> Sv32PagePerms { } } -impl Memory { +impl Sv32Memory { pub fn new(total_size_bytes: usize, page_size: usize) -> Self { assert!(page_size != 0, "page_size must be > 0"); assert!(total_size_bytes != 0, "total_size_bytes must be > 0"); @@ -296,7 +298,7 @@ impl Memory { } } -impl Sv32PageTable for Memory { +impl Sv32PageTable for Sv32Memory { fn page_size(&self) -> usize { self.page_size } @@ -318,19 +320,11 @@ impl Sv32PageTable for Memory { } } -impl Mmu for Memory { +impl MMU for Sv32Memory { fn mem(&self) -> Ref> { self.backing.borrow() } - fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { - Memory::map_range(self, start, len, perms); - } - - fn current_root(&self) -> usize { - self.root_ppn() - } - fn mem_slice( &self, start: VirtualAddress, @@ -471,6 +465,16 @@ impl Mmu for Memory { backing[offset..offset + 4].try_into().unwrap(), )) } +} + +impl API for Sv32Memory { + fn map_range(&self, start: VirtualAddress, len: usize, perms: Perms) { + Sv32Memory::map_range(self, start, len, perms); + } + + fn current_root(&self) -> usize { + self.root_ppn() + } fn size(&self) -> usize { self.total_size() @@ -493,7 +497,7 @@ impl Mmu for Memory { } fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { - Memory::alloc_on_heap(self, data) + Sv32Memory::alloc_on_heap(self, data) } fn next_heap(&self) -> VirtualAddress { diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 5446052..746fc4e 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,4 +1,4 @@ -use crate::cpu::{CPU, CSR_SATP}; +use crate::cpu::CPU; use crate::host_interface::HostInterface; use crate::memory::Memory; use crate::metering::Metering; @@ -50,7 +50,8 @@ impl VM { ) -> Self { let mut cpu = CPU::new(syscall_handler); cpu.regs[Register::Sp as usize] = memory.stack_top().as_u32(); - cpu.csrs.insert(CSR_SATP, memory.satp()); + let satp = memory.satp(); + cpu.set_satp(&memory, satp); Self { cpu, memory, From 00916d37a6f608c3a767a8200355e20e6e33f1f2 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 12:11:46 +0200 Subject: [PATCH 28/70] moved heap ptr out of memory --- crates/bootloader/src/bootloader.rs | 78 +++++++++++++----- crates/bootloader/src/syscalls.rs | 121 ++++++++++++++++++---------- crates/kernel/src/init.rs | 5 +- crates/kernel/src/launch.rs | 6 +- crates/kernel/src/task.rs | 9 ++- crates/types/src/boot.rs | 11 ++- crates/vm/src/memory/mod.rs | 4 +- crates/vm/src/memory/sv32.rs | 57 +------------ crates/vm/src/vm.rs | 9 ++- 9 files changed, 172 insertions(+), 128 deletions(-) diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index aa17c67..27e5c4d 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -1,6 +1,6 @@ use core::{mem, slice}; use core::fmt::Write as FmtWrite; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use std::vec::Vec; @@ -11,7 +11,7 @@ use types::{boot::BootInfo, transaction::TransactionBundle, SV32_DIRECT_MAP_BASE use crate::DefaultSyscallHandler; use state::State; use vm::host_interface::NoopHost; -use vm::memory::{API, Mmu, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; +use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; use vm::registers::Register; use vm::vm::VM; @@ -38,6 +38,7 @@ impl Default for BootConfig { pub struct Bootloader { pub config: BootConfig, memory: Rc, + heap_ptr: Rc>, } impl Bootloader { @@ -45,6 +46,7 @@ impl Bootloader { Self { config: BootConfig::default(), memory: Rc::new(Sv32Memory::new(total_size_bytes, PAGE_SIZE)), + heap_ptr: Rc::new(Cell::new(0)), } } @@ -86,7 +88,7 @@ impl Bootloader { .write_bytes(VirtualAddress(min_base as u32), &image); // Start the heap after the loaded image to avoid overwriting kernel text/rodata let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; - self.memory.set_next_heap(VirtualAddress(heap_start)); + self.set_next_heap(heap_start); // Ensure the kernel has a mapped stack region near the top of memory. let stack_base = self .memory @@ -121,7 +123,14 @@ impl Bootloader { let (entry_point, memory) = self.load_kernel(kernel_elf); let host: Box = Box::new(NoopHost); - let mut vm = VM::new(memory.clone(), host, Box::new(DefaultSyscallHandler::new(state.clone()))); + let mut vm = VM::new( + memory.clone(), + host, + Box::new(DefaultSyscallHandler::with_heap( + state.clone(), + Rc::clone(&self.heap_ptr), + )), + ); vm.cpu.verbose = verbose; if let Some(writer) = verbose_writer { vm.cpu.set_verbose_writer(writer); @@ -141,26 +150,32 @@ impl Bootloader { // Register a length hint so the kernel can bounds-check the payload. vm.set_reg_u32(Register::A1, encoded.len() as u32); // Keep heap aligned after our write. - vm.memory - .set_next_heap(VirtualAddress( - (addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32, - )); + self.set_next_heap( + (addr as usize + encoded.len() + HEAP_PTR_OFFSET as usize) as u32, + ); } fn place_state(&mut self, vm: &mut VM, state: &[u8]) { let addr = self.place_data(vm, Register::A2, state); vm.set_reg_u32(Register::A3, state.len() as u32); - vm.memory - .set_next_heap(VirtualAddress( - (addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32, - )); + self.set_next_heap( + (addr as usize + state.len() + HEAP_PTR_OFFSET as usize) as u32, + ); } fn place_boot_info(&mut self, vm: &mut VM) { // For now the bootloader owns the page tables, so `root_ppn` is a placeholder (0). + let heap_start = self.ensure_heap_ptr(); + let aligned_heap = (heap_start + 7) & !7; + let boot_info_size = mem::size_of::() as u32; + let next_heap = aligned_heap + .checked_add(boot_info_size) + .and_then(|v| v.checked_add(HEAP_PTR_OFFSET)) + .expect("boot info heap pointer overflow"); let boot_info = BootInfo::new( self.memory.current_root() as u32, - vm.memory.stack_top().as_u32(), + vm.memory_api().stack_top().as_u32(), + next_heap, self.memory.size() as u32, self.memory.next_free_ppn() as u32, ); @@ -170,16 +185,41 @@ impl Bootloader { mem::size_of::(), ) }; - let addr = self.place_data(vm, Register::A4, bytes); - vm.memory - .set_next_heap(VirtualAddress( - (addr as usize + bytes.len() + HEAP_PTR_OFFSET as usize) as u32, - )); + let _addr = self.place_data(vm, Register::A4, bytes); + self.set_next_heap(next_heap); } fn place_data(&self, vm: &mut VM, reg: Register, data: &[u8]) -> u32 { - let addr = self.memory.alloc_on_heap(data).as_u32(); + let addr = self.alloc_on_heap(data).as_u32(); vm.cpu.regs[reg as usize] = addr; addr } + + fn ensure_heap_ptr(&self) -> u32 { + let current = self.heap_ptr.get(); + if current == 0 { + self.heap_ptr.set(HEAP_PTR_OFFSET); + HEAP_PTR_OFFSET + } else { + current + } + } + + fn set_next_heap(&self, next: u32) { + self.heap_ptr.set(next); + } + + fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { + let mut addr = self.ensure_heap_ptr(); + let align = 8u32; + addr = (addr + (align - 1)) & !(align - 1); + let end = addr + .checked_add(data.len() as u32) + .expect("heap allocation overflow"); + let start = VirtualAddress(addr); + self.memory.map_range(start, data.len(), Perms::rw_kernel()); + self.memory.write_bytes(start, data); + self.heap_ptr.set(end); + start + } } diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index 5fbecdb..0c127ef 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -1,4 +1,4 @@ -use core::cell::RefCell; +use core::cell::{Cell, RefCell}; use core::fmt::Write; use std::any::Any; use std::rc::Rc; @@ -6,8 +6,8 @@ use std::rc::Rc; use state::State; use types::{ADDRESS_LEN, address::Address, result::RESULT_SIZE}; use vm::host_interface::HostInterface; -use vm::memory::{HEAP_PTR_OFFSET, Memory, VirtualAddress}; -use vm::metering::{MeterResult, Metering}; +use vm::memory::{API, MMU, HEAP_PTR_OFFSET, Memory, Perms, VirtualAddress}; +use vm::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; use vm::registers::Register; use vm::sys_call::{ SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, @@ -32,6 +32,7 @@ enum Arg { pub struct DefaultSyscallHandler { verbose_writer: Option>>, state: Rc>, + heap_ptr: Rc>, } impl std::fmt::Debug for DefaultSyscallHandler { @@ -47,20 +48,73 @@ impl std::fmt::Debug for DefaultSyscallHandler { } impl DefaultSyscallHandler { - pub fn new(state: Rc>) -> Self { - Self { - verbose_writer: None, - state, + fn ensure_heap_ptr(&self) -> u32 { + let current = self.heap_ptr.get(); + if current == 0 { + self.heap_ptr.set(HEAP_PTR_OFFSET); + HEAP_PTR_OFFSET + } else { + current + } + } + + fn set_heap_ptr(&self, next: u32) { + self.heap_ptr.set(next); + } + + fn write_bytes(&self, memory: &Memory, start: VirtualAddress, data: &[u8]) -> bool { + let mut meter = NoopMeter::default(); + for (idx, byte) in data.iter().enumerate() { + let addr = start.wrapping_add(idx as u32); + if !memory.store_u8(addr, *byte, &mut meter, MemoryAccessKind::Store) { + return false; + } } + true + } + + fn alloc_on_heap( + &self, + memory: &Memory, + data: &[u8], + align: u32, + ) -> Option { + let mut addr = self.ensure_heap_ptr(); + let mask = align.checked_sub(1)?; + addr = addr.checked_add(mask)? & !mask; + let end = addr.checked_add(data.len() as u32)?; + let start = VirtualAddress(addr); + memory.map_range(start, data.len(), Perms::rw_kernel()); + if !data.is_empty() && !self.write_bytes(memory, start, data) { + return None; + } + self.set_heap_ptr(end); + Some(start) + } + pub fn new(state: Rc>) -> Self { + Self::with_writer_and_heap(state, None, Rc::new(Cell::new(0))) + } + + pub fn with_heap(state: Rc>, heap_ptr: Rc>) -> Self { + Self::with_writer_and_heap(state, None, heap_ptr) } pub fn with_writer( state: Rc>, writer: Option>>, + ) -> Self { + Self::with_writer_and_heap(state, writer, Rc::new(Cell::new(0))) + } + + fn with_writer_and_heap( + state: Rc>, + writer: Option>>, + heap_ptr: Rc>, ) -> Self { Self { verbose_writer: writer, state, + heap_ptr, } } } @@ -254,7 +308,10 @@ impl DefaultSyscallHandler { if matches!(metering.on_alloc(buf.len()), MeterResult::Halt) { panic!("Metering halted alloc during storage_get"); } - let addr = borrowed_memory.alloc_on_heap(&buf); + let addr = match self.alloc_on_heap(&memory, &buf, 8) { + Some(ptr) => ptr, + None => return 0, + }; println!( "✅ Found value for address: '{}', domain: '{}', Key: '{}'", address_hex, domain, display_key @@ -671,7 +728,10 @@ impl DefaultSyscallHandler { if matches!(metering.on_alloc(result_bytes.len()), MeterResult::Halt) { panic!("Metering halted alloc for call_program result"); } - borrowed_memory.alloc_on_heap(&result_bytes).as_u32() + match self.alloc_on_heap(&memory, &result_bytes, 8) { + Some(ptr) => ptr.as_u32(), + None => 0, + } } } @@ -694,39 +754,15 @@ impl DefaultSyscallHandler { return 0; } - let current_heap = memory.next_heap(); - - // Initialize heap pointer if not set (no code has been written) - if current_heap.as_u32() == 0 { - memory.set_next_heap(VirtualAddress(HEAP_PTR_OFFSET)); - } - // Allocate aligned memory on heap let data = vec![0u8; size]; - let ptr = memory.alloc_on_heap(&data); - - if ptr.as_u32() == 0 { - println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); - return 0; - } - - // Check if allocated address meets alignment requirements - if ptr.as_usize() % align != 0 { - // Re-allocate with enough space for alignment - let total_size = size + align - 1; - let padded_data = vec![0u8; total_size]; - let padded_ptr = memory.alloc_on_heap(&padded_data); - if padded_ptr.as_u32() == 0 { - println!( - "VM Alloc: Out of memory, failed to allocate {} bytes for alignment", - total_size - ); + let ptr = match self.alloc_on_heap(&memory, &data, align as u32) { + Some(ptr) => ptr, + None => { + println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); return 0; } - // Return properly aligned pointer within the allocated region - let aligned_ptr = ((padded_ptr.as_usize() + align - 1) & !(align - 1)) as u32; - return aligned_ptr; - } + }; ptr.as_u32() } @@ -799,7 +835,10 @@ impl DefaultSyscallHandler { }; let bal = host.balance(addr); - memory.alloc_on_heap(&bal.to_le_bytes()).as_u32() + match self.alloc_on_heap(&memory, &bal.to_le_bytes(), 8) { + Some(ptr) => ptr.as_u32(), + None => 0, + } } /// Minimal `brk(2)` implementation: @@ -807,12 +846,12 @@ impl DefaultSyscallHandler { /// - Only moves the break forward; shrink requests are ignored. fn sys_brk(&mut self, args: [u32; 6], memory: Memory, _metering: &mut dyn Metering) -> u32 { let new_brk = args[0]; - let current = memory.next_heap().as_u32(); + let current = self.ensure_heap_ptr(); if new_brk == 0 { return current; } if new_brk >= current { - memory.set_next_heap(VirtualAddress(new_brk)); + self.set_heap_ptr(new_brk); new_brk } else { current diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index bc7e4cf..e72ddc6 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -54,7 +54,7 @@ fn init_boot_info(boot_info_ptr: *const BootInfo) { *PAGE_ALLOC_INIT.get_mut() = true; } } - let task = Task::kernel(info.root_ppn, info.kstack_top); + let task = Task::kernel(info.root_ppn, info.kstack_top, info.heap_ptr); unsafe { let tasks_slot = TASKS.get_mut(); match tasks_slot { @@ -63,9 +63,10 @@ fn init_boot_info(boot_info_ptr: *const BootInfo) { } } logf!( - "boot_info: root_ppn=0x%x kstack_top=0x%x mem_size=%d", + "boot_info: root_ppn=0x%x kstack_top=0x%x heap_ptr=0x%x mem_size=%d", info.root_ppn, info.kstack_top, + info.heap_ptr, info.memory_size ); } else { diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index 9427985..fd8eae4 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -280,7 +280,11 @@ pub fn prep_program_task( tramp_phys_2 as u32 ); - let mut task = Task::new(AddressSpace::new(root_ppn, asid), kstack_top); + let mut task = Task::new( + AddressSpace::new(root_ppn, asid), + kstack_top, + Config::HEAP_START_ADDR as u32, + ); // Set up initial trapframe. let stack_top = PROGRAM_VA_BASE .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); diff --git a/crates/kernel/src/task.rs b/crates/kernel/src/task.rs index f7b3833..74fddb5 100644 --- a/crates/kernel/src/task.rs +++ b/crates/kernel/src/task.rs @@ -44,21 +44,24 @@ pub struct Task { pub addr_space: AddressSpace, /// Kernel stack pointer for this task (top of the task's kernel stack). pub kstack_top: u32, + /// Next heap pointer for this task (virtual address). + pub heap_ptr: u32, } impl Task { - pub fn new(addr_space: AddressSpace, kstack_top: u32) -> Self { + pub fn new(addr_space: AddressSpace, kstack_top: u32, heap_ptr: u32) -> Self { Self { tf: TrapFrame::default(), addr_space, kstack_top, + heap_ptr, } } /// Create the initial kernel task. This represents the supervisor itself: /// - `root_ppn` is the kernel page-table root PPN that will be loaded into satp. /// - `kstack_top` is the top of the kernel stack the kernel will run on. - pub fn kernel(root_ppn: u32, kstack_top: u32) -> Self { - Task::new(AddressSpace::new(root_ppn, 0), kstack_top) + pub fn kernel(root_ppn: u32, kstack_top: u32, heap_ptr: u32) -> Self { + Task::new(AddressSpace::new(root_ppn, 0), kstack_top, heap_ptr) } } diff --git a/crates/types/src/boot.rs b/crates/types/src/boot.rs index b55b56a..0287227 100644 --- a/crates/types/src/boot.rs +++ b/crates/types/src/boot.rs @@ -14,6 +14,8 @@ pub struct BootInfo { pub root_ppn: u32, /// Top of the kernel stack. pub kstack_top: u32, + /// Next heap pointer for kernel allocations (virtual address). + pub heap_ptr: u32, /// Total physical memory size in bytes. pub memory_size: u32, /// First free physical page number after bootloader allocations. @@ -21,10 +23,17 @@ pub struct BootInfo { } impl BootInfo { - pub const fn new(root_ppn: u32, kstack_top: u32, memory_size: u32, next_free_ppn: u32) -> Self { + pub const fn new( + root_ppn: u32, + kstack_top: u32, + heap_ptr: u32, + memory_size: u32, + next_free_ppn: u32, + ) -> Self { Self { root_ppn, kstack_top, + heap_ptr, memory_size, next_free_ppn, } diff --git a/crates/vm/src/memory/mod.rs b/crates/vm/src/memory/mod.rs index aacf7b3..1cab39b 100644 --- a/crates/vm/src/memory/mod.rs +++ b/crates/vm/src/memory/mod.rs @@ -6,6 +6,7 @@ use crate::metering::{MemoryAccessKind, Metering}; mod sv32; pub use sv32::Sv32Memory; +pub use types::mmu::*; pub const HEAP_PTR_OFFSET: u32 = 0x100; @@ -121,11 +122,8 @@ pub trait API: std::fmt::Debug { fn set_satp(&self, satp: u32); /// Top of the stack for this memory layout. fn stack_top(&self) -> VirtualAddress; - fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress; fn size(&self) -> usize; fn offset(&self, addr: VirtualAddress) -> usize; - fn next_heap(&self) -> VirtualAddress; - fn set_next_heap(&self, next: VirtualAddress); } pub trait Mmu: MMU + API {} diff --git a/crates/vm/src/memory/sv32.rs b/crates/vm/src/memory/sv32.rs index 6292e9b..3d46219 100644 --- a/crates/vm/src/memory/sv32.rs +++ b/crates/vm/src/memory/sv32.rs @@ -1,14 +1,13 @@ use std::cell::{Cell, Ref, RefCell}; -use std::collections::HashMap; use std::rc::Rc; +use crate::metering::{MemoryAccessKind, MeterResult, Metering}; + use types::{ map_allocating, map_to_physical, Sv32PagePerms, Sv32PageTable, SV32_PTE_R, SV32_PTE_V, SV32_PTE_W, SV32_PTE_X, SV32_SATP_PPN_MASK, }; -use crate::metering::{MemoryAccessKind, MeterResult, Metering}; - use super::{API, MMU, Perms, VirtualAddress}; /// Software Sv32 MMU backed by a contiguous physical buffer. @@ -21,7 +20,7 @@ use super::{API, MMU, Perms, VirtualAddress}; /// - Mapping APIs (`map_page`/`map_range`) allocate tables/frames and set R/W/X/U bits. /// - `translate` walks VPN1→VPN0, checks permissions against the access kind, and returns a byte /// offset into the backing. All loads/stores go through this path. -/// - A guest heap bump pointer is tracked per-root. +/// - Heap management is handled outside the MMU. /// /// Limitations/assumptions: /// - No unmap or reuse of frames yet; the allocator only grows. @@ -38,8 +37,6 @@ pub struct Sv32Memory { backing: Rc>>, /// satp value that selects the active root PPN. satp: Cell, - /// Per-root heap bump pointer (VA). - per_root_heap: RefCell>, /// Next free physical frame index for frame allocation. next_free_frame: Cell, } @@ -69,7 +66,6 @@ impl Sv32Memory { total_pages, backing: Rc::new(RefCell::new(vec![0u8; total])), satp: Cell::new(root_ppn as u32), - per_root_heap: RefCell::new(HashMap::new()), next_free_frame: Cell::new(root_ppn + 1), }; // Zero the root page table frame so we can immediately populate it. @@ -232,18 +228,6 @@ impl Sv32Memory { ) } - fn next_heap_for_root(&self) -> VirtualAddress { - let key = self.satp.get(); - let mut heaps = self.per_root_heap.borrow_mut(); - *heaps.entry(key).or_insert_with(|| VirtualAddress(0)) - } - - fn set_next_heap_for_root(&self, next: VirtualAddress) { - let key = self.satp.get(); - let mut heaps = self.per_root_heap.borrow_mut(); - heaps.insert(key, next); - } - /// Copy a slice into physical backing, honoring translation and page boundaries. fn copy_into_backing(&self, start: VirtualAddress, data: &[u8], kind: MemoryAccessKind) { let mut remaining = data.len(); @@ -273,29 +257,6 @@ impl Sv32Memory { pub fn write_bytes(&self, start: VirtualAddress, data: &[u8]) { self.copy_into_backing(start, data, MemoryAccessKind::Store); } - - /// Allocate space on the per-root heap, map it writable, and copy data. - pub fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { - let mut addr = self.next_heap_for_root().as_u32(); - let align = 8; - addr = (addr + (align - 1)) & !(align - 1); - let end = addr + data.len() as u32; - let start_va = VirtualAddress(addr); - self.map_range(start_va, data.len(), Perms::rw_kernel()); - - self.copy_into_backing(start_va, data, MemoryAccessKind::Store); - let end_va = VirtualAddress(end); - self.set_next_heap_for_root(end_va); - start_va - } - - pub fn next_heap(&self) -> VirtualAddress { - self.next_heap_for_root() - } - - pub fn set_next_heap(&self, next: VirtualAddress) { - self.set_next_heap_for_root(next); - } } impl Sv32PageTable for Sv32Memory { @@ -495,16 +456,4 @@ impl API for Sv32Memory { fn stack_top(&self) -> VirtualAddress { VirtualAddress(self.total_size() as u32) } - - fn alloc_on_heap(&self, data: &[u8]) -> VirtualAddress { - Sv32Memory::alloc_on_heap(self, data) - } - - fn next_heap(&self) -> VirtualAddress { - self.next_heap_for_root() - } - - fn set_next_heap(&self, next: VirtualAddress) { - self.set_next_heap_for_root(next); - } } diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 746fc4e..f5de154 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,6 +1,6 @@ use crate::cpu::CPU; use crate::host_interface::HostInterface; -use crate::memory::Memory; +use crate::memory::{API, Memory}; use crate::metering::Metering; use crate::registers::Register; use crate::sys_call::SyscallHandler; @@ -67,6 +67,10 @@ impl VM { self.cpu.regs[reg as usize] = data; } + pub fn memory_api(&self) -> Rc { + self.memory.clone() as Rc + } + /// Dumps the entire memory contents for debugging. /// /// EDUCATIONAL PURPOSE: This demonstrates memory inspection tools that @@ -100,10 +104,7 @@ impl VM { assert!(start < end, "invalid memory range"); assert!(end <= borrowed_memory.mem().len(), "range out of bounds"); - // EDUCATIONAL: Show heap pointer for context - let next_heap = borrowed_memory.next_heap(); println!("--- Memory Dump ---"); - println!("Next heap pointer: 0x{:08x}", next_heap.as_u32()); // EDUCATIONAL: Display memory in 16-byte lines for addr in (start..end).step_by(16) { From 119511b486e09f45668dc8212b4375628c512fd3 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 12:27:42 +0200 Subject: [PATCH 29/70] kernel stack ptr fix --- crates/bootloader/src/bootloader.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 27e5c4d..1faab8c 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -17,6 +17,8 @@ use vm::vm::VM; const MIN_KERNEL_MAP_BYTES: usize = 16 * 1024; const KERNEL_STACK_BYTES: usize = 4 * PAGE_SIZE; +const KERNEL_WINDOW_BYTES: usize = 256 * 1024; +const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; /// Boot configuration options consumed by the loader. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -65,6 +67,13 @@ impl Bootloader { let ro_end = (ro_base + rodata.len() as u64) as usize; let image_end = core::cmp::max(code_end, ro_end); let image_size = image_end.checked_sub(min_base).expect("invalid image size"); + let kernel_map_bytes = core::cmp::max(image_size, MIN_KERNEL_MAP_BYTES); + let stack_base = (KERNEL_STACK_TOP as usize).saturating_sub(KERNEL_STACK_BYTES); + + assert!( + kernel_map_bytes <= stack_base, + "kernel image overlaps stack window" + ); assert!( image_end <= self.memory.size(), @@ -83,18 +92,14 @@ impl Bootloader { } self.memory - .map_range(VirtualAddress(min_base as u32), core::cmp::max(image_size, MIN_KERNEL_MAP_BYTES), Perms::rwx_kernel()); + .map_range(VirtualAddress(min_base as u32), kernel_map_bytes, Perms::rwx_kernel()); self.memory .write_bytes(VirtualAddress(min_base as u32), &image); // Start the heap after the loaded image to avoid overwriting kernel text/rodata let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; self.set_next_heap(heap_start); // Ensure the kernel has a mapped stack region near the top of memory. - let stack_base = self - .memory - .stack_top() - .as_usize() - .saturating_sub(KERNEL_STACK_BYTES); + let stack_base = (KERNEL_STACK_TOP as usize).saturating_sub(KERNEL_STACK_BYTES); self.memory .map_range(VirtualAddress(stack_base as u32), KERNEL_STACK_BYTES, Perms::rw_kernel()); // Map a direct window over all physical memory so the kernel can touch @@ -131,6 +136,7 @@ impl Bootloader { Rc::clone(&self.heap_ptr), )), ); + vm.set_reg_u32(Register::Sp, KERNEL_STACK_TOP); vm.cpu.verbose = verbose; if let Some(writer) = verbose_writer { vm.cpu.set_verbose_writer(writer); @@ -174,7 +180,7 @@ impl Bootloader { .expect("boot info heap pointer overflow"); let boot_info = BootInfo::new( self.memory.current_root() as u32, - vm.memory_api().stack_top().as_u32(), + KERNEL_STACK_TOP, next_heap, self.memory.size() as u32, self.memory.next_free_ppn() as u32, From c5dfbc5d47b7b97235e60311204a17c35cb44960 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 14:22:09 +0200 Subject: [PATCH 30/70] kernel: refactor init and task storage --- crates/kernel/src/global.rs | 97 +++++++++++++++++++++++++++++-- crates/kernel/src/init.rs | 32 +++++----- crates/kernel/src/launch.rs | 34 ++++------- crates/kernel/src/mmu.rs | 8 +++ crates/kernel/src/program_call.rs | 20 +++---- crates/kernel/src/task.rs | 10 +--- crates/kernel/src/trap.rs | 3 +- 7 files changed, 140 insertions(+), 64 deletions(-) diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 0c9e40d..8805d2a 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -1,10 +1,9 @@ -extern crate alloc; - -use alloc::vec::Vec; use core::cell::UnsafeCell; +use core::mem::MaybeUninit; +use core::ptr; use state::State; -use crate::{BootInfo, Task}; +use crate::Task; use crate::mmu::PageAllocator; /// Minimal wrapper to store non-`Sync` types in statics. @@ -30,10 +29,96 @@ impl Global { unsafe impl Sync for Global {} +pub const MAX_TASKS: usize = 16; +pub const KERNEL_TASK_SLOT: usize = 0; + +pub struct TaskList { + len: usize, + slots: MaybeUninit<[Task; MAX_TASKS]>, +} + +impl TaskList { + pub const fn new() -> Self { + Self { + len: 0, + slots: MaybeUninit::uninit(), + } + } + + pub fn push(&mut self, task: Task) -> Result<&Task, Task> { + if self.len >= MAX_TASKS { + return Err(task); + } + let idx = self.len; + unsafe { + let base = self.slots.as_mut_ptr() as *mut Task; + base.add(idx).write(task); + } + self.len += 1; + Ok(unsafe { &*(self.slots.as_ptr() as *const Task).add(idx) }) + } + + pub fn get(&self, idx: usize) -> Option<&Task> { + if idx < self.len { + Some(unsafe { &*(self.slots.as_ptr() as *const Task).add(idx) }) + } else { + None + } + } + + pub fn get_mut(&mut self, idx: usize) -> Option<&mut Task> { + if idx < self.len { + Some(unsafe { &mut *(self.slots.as_mut_ptr() as *mut Task).add(idx) }) + } else { + None + } + } + + pub fn kernel_task(&self) -> Option<&Task> { + self.get(KERNEL_TASK_SLOT) + } + + pub fn set_at(&mut self, idx: usize, task: Task) -> Result<&Task, Task> { + if idx >= MAX_TASKS { + return Err(task); + } + if idx > self.len { + return Err(task); + } + if idx < self.len { + unsafe { ptr::drop_in_place((self.slots.as_mut_ptr() as *mut Task).add(idx)) }; + } else { + self.len += 1; + } + unsafe { + let base = self.slots.as_mut_ptr() as *mut Task; + base.add(idx).write(task); + Ok(&*base.add(idx)) + } + } + + pub fn last(&self) -> Option<&Task> { + if self.len == 0 { + None + } else { + self.get(self.len - 1) + } + } +} + +impl Drop for TaskList { + fn drop(&mut self) { + for idx in 0..self.len { + unsafe { + ptr::drop_in_place((self.slots.as_mut_ptr() as *mut Task).add(idx)); + } + } + } +} + #[allow(dead_code)] -pub static TASKS: Global>> = Global::new(None); +pub static TASKS: Global = Global::new(TaskList::new()); pub static STATE: Global> = Global::new(None); -pub static BOOT_INFO: Global> = Global::new(None); pub static PAGE_ALLOC_INIT: Global = Global::new(false); pub static NEXT_ASID: Global = Global::new(1); pub static ROOT_PPN: Global = Global::new(0); diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index e72ddc6..6819f94 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -1,19 +1,21 @@ -use alloc::vec; use core::slice; use program::{log, logf}; use state::State; -use kernel::global::{BOOT_INFO, PAGE_ALLOC_INIT, STATE, TASKS}; +use kernel::global::{KERNEL_TASK_SLOT, PAGE_ALLOC_INIT, STATE, TASKS}; use kernel::{mmu, BootInfo, Task, trap}; /// Initialize kernel state from the bootloader handoff and optional state blob. pub fn init_kernel(state_ptr: *const u8, state_len: usize, boot_info_ptr: *const BootInfo) { - if let Some(info) = unsafe { boot_info_ptr.as_ref() } { + let boot_info = unsafe { boot_info_ptr.as_ref() }; + if let Some(info) = init_boot_info(boot_info) { trap::init_trap_vector(info.kstack_top); + init_state(state_ptr, state_len); + } else { + panic!("init_kernel: missing boot info"); } - init_state(state_ptr, state_len); - init_boot_info(boot_info_ptr); + log!("kernel initialized"); } fn init_state(state_ptr: *const u8, state_len: usize) { @@ -39,27 +41,25 @@ fn init_state(state_ptr: *const u8, state_len: usize) { } } -fn init_boot_info(boot_info_ptr: *const BootInfo) { +fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { logf!( "init_boot_info: boot_info_ptr=0x%x", - boot_info_ptr as usize as u32 + boot_info + .map(|info| info as *const BootInfo as usize as u32) + .unwrap_or(0) ); - if let Some(info) = unsafe { boot_info_ptr.as_ref() } { - unsafe { - *BOOT_INFO.get_mut() = Some(*info); - } + if let Some(info) = boot_info { unsafe { if !*PAGE_ALLOC_INIT.get_mut() { mmu::init(info); *PAGE_ALLOC_INIT.get_mut() = true; } } - let task = Task::kernel(info.root_ppn, info.kstack_top, info.heap_ptr); + let task = Task::kernel(info.root_ppn, info.heap_ptr); unsafe { let tasks_slot = TASKS.get_mut(); - match tasks_slot { - Some(tasks) => tasks.push(task), - None => *tasks_slot = Some(vec![task]), + if tasks_slot.set_at(KERNEL_TASK_SLOT, task).is_err() { + log!("kernel task slot unavailable; kernel task not recorded"); } } logf!( @@ -69,7 +69,9 @@ fn init_boot_info(boot_info_ptr: *const BootInfo) { info.heap_ptr, info.memory_size ); + Some(info) } else { log!("boot_info missing; kernel task not initialized"); + None } } diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index fd8eae4..04f3b8f 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -19,7 +19,7 @@ // jr t1 // This lets us switch satp safely from a VA that stays valid across the root change. // -// prep_program_task(kstack_top, to, from, code, input, entry_off): +// prep_program_task(to, from, code, input, entry_off): // 1) Allocate ASID and a fresh root PPN; map the user window with user_rwx perms. // 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). // 3) Map the trampoline page into the user root and mirror the same physical page @@ -44,7 +44,7 @@ // when modeling fuller privilege transitions. use crate::{AddressSpace, Config, Task, mmu}; -use crate::global::{BOOT_INFO, NEXT_ASID, TASKS}; +use crate::global::{KERNEL_TASK_SLOT, NEXT_ASID, TASKS}; use program::log; use program::logf; use types::{address::Address, ADDRESS_LEN}; @@ -93,12 +93,11 @@ const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; /// /// This sets up: /// - Maps a fixed VA window [PROGRAM_VA_BASE, PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES). -/// - Returns a Task with the new address space and provided kernel stack top. +/// - Returns a Task with the new address space. /// /// The caller is responsible for copying program bytes into the mapped window /// and initializing the user trapframe (PC/SP/args) before running. pub fn prep_program_task( - kstack_top: u32, to: &Address, from: &Address, code: &[u8], @@ -114,13 +113,7 @@ pub fn prep_program_task( // kernel heap already consumed (account code lives there). We conservatively // push the allocator toward the top of memory so new user roots/tables // don't overlap heap-backed data. - let total_ppn = unsafe { - BOOT_INFO - .get_mut() - .as_ref() - .map(|b| b.memory_size / PAGE_SIZE as u32) - .unwrap_or(0) - }; + let total_ppn = mmu::total_ppn().unwrap_or(0); let reserve = (PROGRAM_WINDOW_BYTES / PAGE_SIZE) as u32 + 4; // user window + a few tables if total_ppn > reserve { let min_ppn = total_ppn - reserve; @@ -280,11 +273,7 @@ pub fn prep_program_task( tramp_phys_2 as u32 ); - let mut task = Task::new( - AddressSpace::new(root_ppn, asid), - kstack_top, - Config::HEAP_START_ADDR as u32, - ); + let mut task = Task::new(AddressSpace::new(root_ppn, asid), Config::HEAP_START_ADDR as u32); // Set up initial trapframe. let stack_top = PROGRAM_VA_BASE .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); @@ -346,13 +335,12 @@ pub fn run_task(task: &Task) { ); // Stash the kernel context so a future return path could restore it. unsafe { - if let Some(tasks) = TASKS.get_mut() { - if let Some(kernel_task) = tasks.get_mut(0) { - kernel_task.addr_space.root_ppn = kernel_root; - kernel_task.tf.regs[REG_SP] = saved_sp; - kernel_task.tf.regs[REG_RA] = saved_ra; - kernel_task.tf.pc = saved_pc; - } + let tasks = TASKS.get_mut(); + if let Some(kernel_task) = tasks.get_mut(KERNEL_TASK_SLOT) { + kernel_task.addr_space.root_ppn = kernel_root; + kernel_task.tf.regs[REG_SP] = saved_sp; + kernel_task.tf.regs[REG_RA] = saved_ra; + kernel_task.tf.pc = saved_pc; } } // Update the helper's view of the current root before switching. diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/mmu.rs index c4c5e4b..2df0274 100644 --- a/crates/kernel/src/mmu.rs +++ b/crates/kernel/src/mmu.rs @@ -55,6 +55,10 @@ impl PageAllocator { self.next_ppn = min_ppn; } } + + pub fn limit_ppn(&self) -> u32 { + self.limit_ppn + } } /// Return the kernel's current root PPN (satp PPN field). @@ -100,6 +104,10 @@ pub fn bump_page_allocator(min_ppn: u32) { } } +pub fn total_ppn() -> Option { + unsafe { PAGE_ALLOC.get_mut().as_ref().map(|alloc| alloc.limit_ppn()) } +} + /// Map a user-visible virtual range with the provided permissions into a specific root. pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { let alloc = unsafe { PAGE_ALLOC.get_mut() }; diff --git a/crates/kernel/src/program_call.rs b/crates/kernel/src/program_call.rs index 5ca9dde..57934a4 100644 --- a/crates/kernel/src/program_call.rs +++ b/crates/kernel/src/program_call.rs @@ -1,7 +1,7 @@ -use alloc::{format, vec}; +use alloc::format; use kernel::{prep_program_task, run_task, Config, PROGRAM_WINDOW_BYTES}; -use kernel::global::{BOOT_INFO, STATE, TASKS}; +use kernel::global::{STATE, TASKS}; use program::{log, logf}; use state::State; use types::transaction::Transaction; @@ -67,12 +67,10 @@ pub(crate) fn program_call(tx: &Transaction) { ) ); - let kstack_top = unsafe { BOOT_INFO.get_mut().as_ref().map(|b| b.kstack_top).unwrap_or(0) }; - let entry_off = first_nz as u32; if let Some(task) = - prep_program_task(kstack_top, &tx.to, &tx.from, &account.code, &tx.data, entry_off) + prep_program_task(&tx.to, &tx.from, &account.code, &tx.data, entry_off) { logf!( "Program task created: root=0x%x asid=%d window_size=%d", @@ -82,14 +80,12 @@ pub(crate) fn program_call(tx: &Transaction) { ); unsafe { let tasks_slot = TASKS.get_mut(); - match tasks_slot { - Some(tasks) => tasks.push(task), - None => *tasks_slot = Some(vec![task]), + if tasks_slot.push(task).is_err() { + log!("program task list full; skipping run"); + return; } - if let Some(tasks) = tasks_slot { - if let Some(last) = tasks.last() { - run_task(last); - } + if let Some(last) = tasks_slot.last() { + run_task(last); } } } else { diff --git a/crates/kernel/src/task.rs b/crates/kernel/src/task.rs index 74fddb5..3165741 100644 --- a/crates/kernel/src/task.rs +++ b/crates/kernel/src/task.rs @@ -42,26 +42,22 @@ pub struct Task { pub tf: TrapFrame, /// Address space for this task (page-table root/asid). pub addr_space: AddressSpace, - /// Kernel stack pointer for this task (top of the task's kernel stack). - pub kstack_top: u32, /// Next heap pointer for this task (virtual address). pub heap_ptr: u32, } impl Task { - pub fn new(addr_space: AddressSpace, kstack_top: u32, heap_ptr: u32) -> Self { + pub fn new(addr_space: AddressSpace, heap_ptr: u32) -> Self { Self { tf: TrapFrame::default(), addr_space, - kstack_top, heap_ptr, } } /// Create the initial kernel task. This represents the supervisor itself: /// - `root_ppn` is the kernel page-table root PPN that will be loaded into satp. - /// - `kstack_top` is the top of the kernel stack the kernel will run on. - pub fn kernel(root_ppn: u32, kstack_top: u32, heap_ptr: u32) -> Self { - Task::new(AddressSpace::new(root_ppn, 0), kstack_top, heap_ptr) + pub fn kernel(root_ppn: u32, heap_ptr: u32) -> Self { + Task::new(AddressSpace::new(root_ppn, 0), heap_ptr) } } diff --git a/crates/kernel/src/trap.rs b/crates/kernel/src/trap.rs index 2d68763..5c2e90b 100644 --- a/crates/kernel/src/trap.rs +++ b/crates/kernel/src/trap.rs @@ -8,6 +8,8 @@ const SAVED_REG_COUNT: usize = 11; /// Install the kernel trap vector and set up the kernel stack for traps. pub fn init_trap_vector(kstack_top: u32) { + // Seed sscratch with the kernel stack top so trap entry can swap sp with + // sscratch and immediately land on a known-good kernel stack. logf!("init_trap_vector: kstack_top=0x%x", kstack_top); unsafe { asm!("csrw sscratch, {0}", in(reg) kstack_top); @@ -75,7 +77,6 @@ pub extern "C" fn handle_trap(saved: *mut u32) { let scause = read_scause(); let stval = read_stval(); let sepc = regs[0]; - let satp = read_satp(); let sp: u32; unsafe { asm!("mv {0}, sp", out(reg) sp); } From bb487f6a4ae4f00d1dfcdcf720cfe06394855910 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 15:22:27 +0200 Subject: [PATCH 31/70] bootloader: map kernel bss --- crates/bootloader/src/bootloader.rs | 27 +++++++++- crates/compiler/src/elf.rs | 33 ++++++++++++- crates/kernel/src/global.rs | 5 ++ crates/kernel/src/init.rs | 10 ++-- crates/kernel/src/launch.rs | 46 ++++++++++++----- crates/kernel/src/program_call.rs | 5 +- crates/kernel/src/syscall.rs | 46 +++++++++++++++-- crates/program/src/log.rs | 1 + crates/state/tests/state_codec.rs | 76 +++++++++++++++++++++++++++++ 9 files changed, 220 insertions(+), 29 deletions(-) create mode 100644 crates/state/tests/state_codec.rs diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 1faab8c..5957d41 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -61,11 +61,30 @@ impl Bootloader { let (code, code_base) = elf.get_flat_code().expect("kernel ELF missing .text"); let (rodata, ro_base) = elf.get_flat_rodata().unwrap_or((Vec::new(), code_base)); + let (bss, bss_base) = elf.get_flat_bss().unwrap_or((Vec::new(), code_base)); + println!( + "kernel elf: text_base=0x{:08x} text_len=0x{:x} ro_base=0x{:08x} ro_len=0x{:x} bss_base=0x{:08x} bss_len=0x{:x}", + code_base as u32, + code.len(), + ro_base as u32, + rodata.len(), + bss_base as u32, + bss.len() + ); - let min_base = core::cmp::min(code_base, ro_base) as usize; + let mut min_base = core::cmp::min(code_base, ro_base) as usize; + if !bss.is_empty() { + min_base = core::cmp::min(min_base, bss_base as usize); + } let code_end = (code_base + code.len() as u64) as usize; let ro_end = (ro_base + rodata.len() as u64) as usize; - let image_end = core::cmp::max(code_end, ro_end); + let mut image_end = core::cmp::max(code_end, ro_end); + if !bss.is_empty() { + let bss_end = bss_base + .checked_add(bss.len() as u64) + .expect("bss end overflow") as usize; + image_end = core::cmp::max(image_end, bss_end); + } let image_size = image_end.checked_sub(min_base).expect("invalid image size"); let kernel_map_bytes = core::cmp::max(image_size, MIN_KERNEL_MAP_BYTES); let stack_base = (KERNEL_STACK_TOP as usize).saturating_sub(KERNEL_STACK_BYTES); @@ -90,6 +109,10 @@ impl Bootloader { let ro_off = (ro_base as usize).saturating_sub(min_base); image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); } + if !bss.is_empty() { + let bss_off = (bss_base as usize).saturating_sub(min_base); + image[bss_off..bss_off + bss.len()].copy_from_slice(&bss); + } self.memory .map_range(VirtualAddress(min_base as u32), kernel_map_bytes, Perms::rwx_kernel()); diff --git a/crates/compiler/src/elf.rs b/crates/compiler/src/elf.rs index 23af11d..454f792 100644 --- a/crates/compiler/src/elf.rs +++ b/crates/compiler/src/elf.rs @@ -1,4 +1,5 @@ use goblin::elf::Elf; +use goblin::elf::section_header::SHT_NOBITS; pub struct ElfInfo<'a> { pub code: &'a [u8], @@ -68,6 +69,29 @@ impl<'a> ElfInfo<'a> { pub fn get_section_by_name(&self, name: &str) -> Option<&ElfSection<'a>> { self.sections.iter().find(|s| s.name == name) } + + /// Returns a flat `.bss` range (length is zeroed by loader), and base address. + pub fn get_flat_bss(&self) -> Option<(Vec, u64)> { + let bss_sections: Vec<&ElfSection> = self + .sections + .iter() + .filter(|s| s.name.starts_with(".bss") || s.name.starts_with(".sbss")) + .collect(); + + if bss_sections.is_empty() { + return None; + } + + let min_addr = bss_sections.iter().map(|s| s.addr).min().unwrap(); + let max_addr = bss_sections + .iter() + .map(|s| s.addr + s.size) + .max() + .unwrap(); + + let total_size = (max_addr - min_addr) as usize; + Some((vec![0u8; total_size], min_addr)) + } } @@ -80,8 +104,13 @@ pub fn parse_elf_from_bytes<'a>(bytes: &'a [u8]) -> Result, goblin:: let offset = section.sh_offset as usize; let size = section.sh_size as usize; - if offset + size <= bytes.len() { - let data = &bytes[offset..offset + size]; + let is_nobits = section.sh_type == SHT_NOBITS; + if offset + size <= bytes.len() || is_nobits { + let data = if is_nobits { + &bytes[0..0] + } else { + &bytes[offset..offset + size] + }; sections.push(ElfSection { name: name.to_string(), addr: section.sh_addr, diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 8805d2a..3854b09 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -31,6 +31,7 @@ unsafe impl Sync for Global {} pub const MAX_TASKS: usize = 16; pub const KERNEL_TASK_SLOT: usize = 0; +pub static CURRENT_TASK: Global = Global::new(KERNEL_TASK_SLOT); pub struct TaskList { len: usize, @@ -45,6 +46,10 @@ impl TaskList { } } + pub fn len(&self) -> usize { + self.len + } + pub fn push(&mut self, task: Task) -> Result<&Task, Task> { if self.len >= MAX_TASKS { return Err(task); diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index 6819f94..35fbaed 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -1,9 +1,9 @@ -use core::slice; +use core::{cmp, slice}; use program::{log, logf}; use state::State; -use kernel::global::{KERNEL_TASK_SLOT, PAGE_ALLOC_INIT, STATE, TASKS}; +use kernel::global::{CURRENT_TASK, KERNEL_TASK_SLOT, PAGE_ALLOC_INIT, STATE, TASKS}; use kernel::{mmu, BootInfo, Task, trap}; /// Initialize kernel state from the bootloader handoff and optional state blob. @@ -19,11 +19,6 @@ pub fn init_kernel(state_ptr: *const u8, state_len: usize, boot_info_ptr: *const } fn init_state(state_ptr: *const u8, state_len: usize) { - logf!( - "init_state: state_ptr=0x%x state_len=%d", - state_ptr as usize as u32, - state_len as u32 - ); unsafe { let state_slot = STATE.get_mut(); if !state_ptr.is_null() && state_len > 0 { @@ -61,6 +56,7 @@ fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { if tasks_slot.set_at(KERNEL_TASK_SLOT, task).is_err() { log!("kernel task slot unavailable; kernel task not recorded"); } + *CURRENT_TASK.get_mut() = KERNEL_TASK_SLOT; } logf!( "boot_info: root_ppn=0x%x kstack_top=0x%x heap_ptr=0x%x mem_size=%d", diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/launch.rs index 04f3b8f..e19527e 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/launch.rs @@ -44,7 +44,7 @@ // when modeling fuller privilege transitions. use crate::{AddressSpace, Config, Task, mmu}; -use crate::global::{KERNEL_TASK_SLOT, NEXT_ASID, TASKS}; +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, NEXT_ASID, TASKS}; use program::log; use program::logf; use types::{address::Address, ADDRESS_LEN}; @@ -307,16 +307,38 @@ pub fn prep_program_task( /// One-way context switch into a user task: /// - Saves the current kernel frame into TASKS[0] /// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) -pub fn run_task(task: &Task) { +pub fn run_task(task_idx: usize) { + let (target_root, asid, pc, sp, a0, a1, a2, a3) = unsafe { + let tasks = TASKS.get_mut(); + let task = match tasks.get(task_idx) { + Some(task) => task, + None => { + logf!("run_task: invalid task slot %d", task_idx as u32); + return; + } + }; + ( + task.addr_space.root_ppn, + task.addr_space.asid, + task.tf.pc, + task.tf.regs[REG_SP], + task.tf.regs[REG_A0], + task.tf.regs[REG_A1], + task.tf.regs[REG_A2], + task.tf.regs[REG_A3], + ) + }; + unsafe { + *CURRENT_TASK.get_mut() = task_idx; + } let kernel_root = mmu::current_root(); - let target_root = task.addr_space.root_ppn; logf!( "run_task: switching satp 0x%x -> 0x%x asid=%d pc=0x%x sp=0x%x", kernel_root, target_root, - task.addr_space.asid as u32, - task.tf.pc, - task.tf.regs[REG_SP], + asid as u32, + pc, + sp, ); // Save the current kernel frame (SP/RA/PC) into the kernel task slot (index 0). let mut saved_sp: u32; @@ -360,12 +382,12 @@ pub fn run_task(task: &Task) { "mv a3, {a3}", "jr {tramp}", satp = in(reg) target_root, - pc = in(reg) task.tf.pc, - sp = in(reg) task.tf.regs[REG_SP], - a0 = in(reg) task.tf.regs[REG_A0], - a1 = in(reg) task.tf.regs[REG_A1], - a2 = in(reg) task.tf.regs[REG_A2], - a3 = in(reg) task.tf.regs[REG_A3], + pc = in(reg) pc, + sp = in(reg) sp, + a0 = in(reg) a0, + a1 = in(reg) a1, + a2 = in(reg) a2, + a3 = in(reg) a3, tramp = in(reg) TRAMPOLINE_VA, options(noreturn) ); diff --git a/crates/kernel/src/program_call.rs b/crates/kernel/src/program_call.rs index 57934a4..ee2c661 100644 --- a/crates/kernel/src/program_call.rs +++ b/crates/kernel/src/program_call.rs @@ -84,9 +84,8 @@ pub(crate) fn program_call(tx: &Transaction) { log!("program task list full; skipping run"); return; } - if let Some(last) = tasks_slot.last() { - run_task(last); - } + let current = tasks_slot.len().saturating_sub(1); + run_task(current); } } else { log!("Program call skipped: no memory manager installed"); diff --git a/crates/kernel/src/syscall.rs b/crates/kernel/src/syscall.rs index a5da543..337fdb6 100644 --- a/crates/kernel/src/syscall.rs +++ b/crates/kernel/src/syscall.rs @@ -3,6 +3,8 @@ //! land here; for now they panic to make missing pieces explicit. use program::{log, logf}; +use crate::global::{CURRENT_TASK, TASKS}; + pub const SYSCALL_STORAGE_GET: u32 = 1; pub const SYSCALL_STORAGE_SET: u32 = 2; pub const SYSCALL_PANIC: u32 = 3; @@ -65,9 +67,47 @@ fn sys_fire_event(_args: [u32; 6]) -> u32 { 0 } -fn sys_alloc(_args: [u32; 6]) -> u32 { - log!("sys_alloc: need implementation"); - 0 +fn sys_alloc(args: [u32; 6]) -> u32 { + let size = args[0]; + let align = args[1]; + + if size == 0 { + log!("sys_alloc: invalid size 0"); + return 0; + } + if align == 0 || (align & (align - 1)) != 0 { + logf!("sys_alloc: invalid alignment %d", align); + return 0; + } + + let current = unsafe { *CURRENT_TASK.get_mut() }; + let tasks = unsafe { TASKS.get_mut() }; + let task = match tasks.get_mut(current) { + Some(task) => task, + None => { + logf!("sys_alloc: no current task for slot %d", current as u32); + return 0; + } + }; + + let mask = align - 1; + let start = match task.heap_ptr.checked_add(mask) { + Some(addr) => addr & !mask, + None => { + log!("sys_alloc: heap ptr overflow"); + return 0; + } + }; + let end = match start.checked_add(size) { + Some(end) => end, + None => { + log!("sys_alloc: size overflow"); + return 0; + } + }; + + task.heap_ptr = end; + start } fn sys_dealloc(_args: [u32; 6]) -> u32 { diff --git a/crates/program/src/log.rs b/crates/program/src/log.rs index 16c4978..44ce687 100644 --- a/crates/program/src/log.rs +++ b/crates/program/src/log.rs @@ -10,6 +10,7 @@ macro_rules! logf_syscall { in("a2") $fmt_len, in("a3") $args_ptr, in("a4") $args_len, + clobber_abi("C"), ); } #[cfg(not(target_arch = "riscv32"))] diff --git a/crates/state/tests/state_codec.rs b/crates/state/tests/state_codec.rs new file mode 100644 index 0000000..0b352df --- /dev/null +++ b/crates/state/tests/state_codec.rs @@ -0,0 +1,76 @@ +use std::collections::BTreeMap; +use std::string::String; +use std::vec::Vec; + +use state::{Account, State}; +use types::address::Address; + +fn assert_account_eq(expected: &Account, actual: &Account) { + assert_eq!(expected.nonce, actual.nonce); + assert_eq!(expected.balance, actual.balance); + assert_eq!(expected.code, actual.code); + assert_eq!(expected.is_contract, actual.is_contract); + assert_eq!(expected.storage, actual.storage); +} + +#[test] +fn encode_decode_empty_state() { + let state = State::new(); + let encoded = state.encode(); + let decoded = State::decode(&encoded).expect("decode empty state"); + assert!(decoded.accounts.is_empty()); +} + +#[test] +fn encode_decode_with_account_and_storage() { + let mut state = State::new(); + let addr = Address([ + 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, + ]); + let mut storage = BTreeMap::new(); + storage.insert(String::from("key"), vec![0xde, 0xad, 0xbe, 0xef]); + storage.insert(String::from("empty"), Vec::new()); + let account = Account { + nonce: 42, + balance: 123_456_789, + code: vec![0xaa, 0xbb, 0xcc], + is_contract: true, + storage, + }; + state.accounts.insert(addr, account.clone()); + + let encoded = state.encode(); + let decoded = State::decode(&encoded).expect("decode populated state"); + + let decoded_account = decoded.accounts.get(&addr).expect("account exists"); + assert_account_eq(&account, decoded_account); +} + +#[test] +fn decode_truncated_bytes_returns_none() { + let mut state = State::new(); + let addr = Address([0x11; 20]); + state.accounts.insert( + addr, + Account { + nonce: 1, + balance: 2, + code: vec![0x42], + is_contract: false, + storage: BTreeMap::new(), + }, + ); + let encoded = state.encode(); + let truncated = &encoded[..encoded.len().saturating_sub(1)]; + assert!(State::decode(truncated).is_none()); +} + +#[test] +fn decode_zero_count_header_returns_empty_state() { + let bytes = [0u8; 4]; + let decoded = State::decode(&bytes).expect("decode zero-count header"); + assert!(decoded.accounts.is_empty()); +} From d99c4ab38b969e9e61a566005e752774064e62a6 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 15:34:31 +0200 Subject: [PATCH 32/70] fix kernel alloc mapping --- crates/kernel/src/bundle.rs | 1 + crates/kernel/src/mmu.rs | 18 ++++++++++++++++++ crates/kernel/src/syscall.rs | 6 ++++++ 3 files changed, 25 insertions(+) diff --git a/crates/kernel/src/bundle.rs b/crates/kernel/src/bundle.rs index 34aac24..d63e07c 100644 --- a/crates/kernel/src/bundle.rs +++ b/crates/kernel/src/bundle.rs @@ -7,6 +7,7 @@ use crate::create_account::create_account; use crate::program_call::program_call; pub(crate) fn process_bundle(encoded_bundle: &[u8]) { + log!("processing transaction bundle"); if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { let count = bundle.transactions.len(); logf!("decoded tx count=%d", count as u32); diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/mmu.rs index 2df0274..350071a 100644 --- a/crates/kernel/src/mmu.rs +++ b/crates/kernel/src/mmu.rs @@ -126,6 +126,24 @@ pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { map_user_range_for_root(root, va_start, len, perms) } +/// Map a kernel-only virtual range with the provided permissions into a specific root. +pub fn map_kernel_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + let alloc = unsafe { PAGE_ALLOC.get_mut() }; + match alloc { + Some(alloc) => { + let mapper = KernelMapper::new(alloc); + map_allocating(&mapper, root_ppn, va_start, len, perms) + } + None => false, + } +} + +/// Map a kernel-only virtual range with the provided permissions into the current root. +pub fn map_kernel_range(va_start: u32, len: usize, perms: PagePerms) -> bool { + let root = unsafe { *ROOT_PPN.get_mut() }; + map_kernel_range_for_root(root, va_start, len, perms) +} + /// Map a VA range in `root_ppn` to an explicit physical range (no allocation). pub fn map_physical_range_for_root( root_ppn: u32, diff --git a/crates/kernel/src/syscall.rs b/crates/kernel/src/syscall.rs index 337fdb6..e266821 100644 --- a/crates/kernel/src/syscall.rs +++ b/crates/kernel/src/syscall.rs @@ -4,6 +4,7 @@ use program::{log, logf}; use crate::global::{CURRENT_TASK, TASKS}; +use crate::mmu; pub const SYSCALL_STORAGE_GET: u32 = 1; pub const SYSCALL_STORAGE_SET: u32 = 2; @@ -106,6 +107,11 @@ fn sys_alloc(args: [u32; 6]) -> u32 { } }; + let len = end.saturating_sub(start) as usize; + if !mmu::map_kernel_range(start, len, mmu::PagePerms::kernel_rw()) { + log!("sys_alloc: failed to map heap range"); + return 0; + } task.heap_ptr = end; start } From 7ee18de1372e3f7b18976a32f32cf3d7b4d75091 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 17:27:08 +0200 Subject: [PATCH 33/70] sys panic --- crates/kernel/src/create_account.rs | 29 +-- crates/kernel/src/lib.rs | 3 +- crates/kernel/src/syscall/alloc.rs | 57 +++++ .../kernel/src/{syscall.rs => syscall/mod.rs} | 66 +----- crates/kernel/src/syscall/panic.rs | 75 +++++++ crates/kernel/src/task/mod.rs | 105 ++++++++++ crates/kernel/src/{launch.rs => task/prep.rs} | 194 +----------------- crates/kernel/src/task/run.rs | 95 +++++++++ crates/kernel/src/{ => task}/task.rs | 0 crates/program/src/panic.rs | 4 +- 10 files changed, 364 insertions(+), 264 deletions(-) create mode 100644 crates/kernel/src/syscall/alloc.rs rename crates/kernel/src/{syscall.rs => syscall/mod.rs} (58%) create mode 100644 crates/kernel/src/syscall/panic.rs create mode 100644 crates/kernel/src/task/mod.rs rename crates/kernel/src/{launch.rs => task/prep.rs} (51%) create mode 100644 crates/kernel/src/task/run.rs rename crates/kernel/src/{ => task}/task.rs (100%) diff --git a/crates/kernel/src/create_account.rs b/crates/kernel/src/create_account.rs index 77a97b2..b6bf9ff 100644 --- a/crates/kernel/src/create_account.rs +++ b/crates/kernel/src/create_account.rs @@ -1,8 +1,7 @@ -use alloc::format; - use kernel::global::STATE; use kernel::Config; -use program::log; +use program::logf; +use program::parser::HexCodec; use state::State; use types::transaction::Transaction; @@ -10,12 +9,15 @@ pub(crate) fn create_account(tx: &Transaction) { let code_size = tx.data.len(); let is_contract = code_size > 0; - let msg = format!( - "Tx creating account at address {}. Is contract: {}. Code size: {} bytes.", - tx.to, is_contract, code_size + let mut addr_buf = [0u8; 40]; + let addr_hex = HexCodec::encode(tx.to.as_ref(), &mut addr_buf); + logf!( + "Tx creating account at address %s. Is contract: %d. Code size: %d bytes.", + addr_hex.as_ptr() as u32, + addr_hex.len() as u32, + is_contract as u32, + code_size as u32 ); - let msg_ref: &str = msg.as_str(); - log!(msg_ref); let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; if code_size > max { @@ -29,10 +31,11 @@ pub(crate) fn create_account(tx: &Transaction) { let account = state.get_account_mut(&tx.to); account.code = tx.data.clone(); account.is_contract = is_contract; - let msg = format!( - "account created in kernel state: addr={} is_contract={} code_len={}", - tx.to, is_contract, code_size + logf!( + "account created in kernel state: addr=%s is_contract=%d code_len=%d", + addr_hex.as_ptr() as u32, + addr_hex.len() as u32, + is_contract as u32, + code_size as u32 ); - let msg_ref: &str = msg.as_str(); - log!(msg_ref); } diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 61bd32e..53be796 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -7,8 +7,7 @@ pub use types::boot::BootInfo; pub mod global; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; -pub mod launch; -pub use launch::{prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; +pub use task::{prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; pub mod mmu; pub mod trap; pub mod syscall; diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs new file mode 100644 index 0000000..113feec --- /dev/null +++ b/crates/kernel/src/syscall/alloc.rs @@ -0,0 +1,57 @@ +use program::{log, logf}; + +use crate::global::{CURRENT_TASK, TASKS}; +use crate::mmu; + +pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { + let size = args[0]; + let align = args[1]; + + if size == 0 { + log!("sys_alloc: invalid size 0"); + return 0; + } + if align == 0 || (align & (align - 1)) != 0 { + logf!("sys_alloc: invalid alignment %d", align); + return 0; + } + + let current = unsafe { *CURRENT_TASK.get_mut() }; + let tasks = unsafe { TASKS.get_mut() }; + let task = match tasks.get_mut(current) { + Some(task) => task, + None => { + logf!("sys_alloc: no current task for slot %d", current as u32); + return 0; + } + }; + + let mask = align - 1; + let start = match task.heap_ptr.checked_add(mask) { + Some(addr) => addr & !mask, + None => { + log!("sys_alloc: heap ptr overflow"); + return 0; + } + }; + let end = match start.checked_add(size) { + Some(end) => end, + None => { + log!("sys_alloc: size overflow"); + return 0; + } + }; + + let len = end.saturating_sub(start) as usize; + if !mmu::map_kernel_range(start, len, mmu::PagePerms::kernel_rw()) { + log!("sys_alloc: failed to map heap range"); + return 0; + } + task.heap_ptr = end; + start +} + +pub(crate) fn sys_dealloc(_args: [u32; 6]) -> u32 { + // No-op: kernel heap is bump-only for now. + 0 +} diff --git a/crates/kernel/src/syscall.rs b/crates/kernel/src/syscall/mod.rs similarity index 58% rename from crates/kernel/src/syscall.rs rename to crates/kernel/src/syscall/mod.rs index e266821..08eb769 100644 --- a/crates/kernel/src/syscall.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -3,8 +3,12 @@ //! land here; for now they panic to make missing pieces explicit. use program::{log, logf}; -use crate::global::{CURRENT_TASK, TASKS}; -use crate::mmu; +pub mod alloc; +pub mod panic; + +use alloc::{sys_alloc, sys_dealloc}; +use panic::sys_panic; +pub(crate) use panic::sys_panic_with_message; pub const SYSCALL_STORAGE_GET: u32 = 1; pub const SYSCALL_STORAGE_SET: u32 = 2; @@ -48,11 +52,6 @@ fn sys_storage_set(_args: [u32; 6]) -> u32 { 0 } -fn sys_panic(_args: [u32; 6]) -> u32 { - log!("sys_panic: need implementation"); - 0 -} - fn sys_log(_args: [u32; 6]) -> u32 { log!("sys_log: need implementation"); 0 @@ -68,59 +67,6 @@ fn sys_fire_event(_args: [u32; 6]) -> u32 { 0 } -fn sys_alloc(args: [u32; 6]) -> u32 { - let size = args[0]; - let align = args[1]; - - if size == 0 { - log!("sys_alloc: invalid size 0"); - return 0; - } - if align == 0 || (align & (align - 1)) != 0 { - logf!("sys_alloc: invalid alignment %d", align); - return 0; - } - - let current = unsafe { *CURRENT_TASK.get_mut() }; - let tasks = unsafe { TASKS.get_mut() }; - let task = match tasks.get_mut(current) { - Some(task) => task, - None => { - logf!("sys_alloc: no current task for slot %d", current as u32); - return 0; - } - }; - - let mask = align - 1; - let start = match task.heap_ptr.checked_add(mask) { - Some(addr) => addr & !mask, - None => { - log!("sys_alloc: heap ptr overflow"); - return 0; - } - }; - let end = match start.checked_add(size) { - Some(end) => end, - None => { - log!("sys_alloc: size overflow"); - return 0; - } - }; - - let len = end.saturating_sub(start) as usize; - if !mmu::map_kernel_range(start, len, mmu::PagePerms::kernel_rw()) { - log!("sys_alloc: failed to map heap range"); - return 0; - } - task.heap_ptr = end; - start -} - -fn sys_dealloc(_args: [u32; 6]) -> u32 { - log!("sys_dealloc: need implementation"); - 0 -} - fn sys_transfer(_args: [u32; 6]) -> u32 { log!("sys_transfer: need implementation"); 0 diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs new file mode 100644 index 0000000..eb5358f --- /dev/null +++ b/crates/kernel/src/syscall/panic.rs @@ -0,0 +1,75 @@ +use program::{log, logf}; +use types::{SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; + +use crate::global::{CURRENT_TASK, TASKS}; +use crate::mmu; + +pub(crate) fn sys_panic_with_message(msg_ptr: u32, msg_len: u32) -> u32 { + if msg_ptr == 0 || msg_len == 0 { + log!("sys_panic: empty message"); + halt(); + } + + let current = unsafe { *CURRENT_TASK.get_mut() }; + let tasks = unsafe { TASKS.get_mut() }; + let task = match tasks.get(current) { + Some(task) => task, + None => { + logf!("sys_panic: no current task for slot %d", current as u32); + halt(); + } + }; + let root_ppn = task.addr_space.root_ppn; + + let mut buf = [0u8; 256]; + let mut remaining = core::cmp::min(msg_len as usize, buf.len()); + let mut dst_off = 0usize; + let mut va = msg_ptr; + while remaining > 0 { + let phys = match mmu::translate_user_va(root_ppn, va) { + Some(p) => p, + None => { + logf!("sys_panic: invalid msg ptr 0x%x", va); + halt(); + } + }; + let page_off = (va as usize) & (SV32_PAGE_SIZE - 1); + let to_copy = core::cmp::min(remaining, SV32_PAGE_SIZE - page_off); + let src = SV32_DIRECT_MAP_BASE as usize + phys; + unsafe { + core::ptr::copy_nonoverlapping( + src as *const u8, + buf.as_mut_ptr().add(dst_off), + to_copy, + ); + } + remaining -= to_copy; + dst_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + + let msg = &buf[..dst_off]; + logf!( + "sys_panic: %s", + msg.as_ptr() as u32, + msg.len() as u32 + ); + if let Ok(s) = core::str::from_utf8(msg) { + logf!("guest panic: %s", s.as_ptr() as u32, s.len() as u32); + } else { + log!("guest panic"); + } + halt(); +} + +pub(crate) fn sys_panic(args: [u32; 6]) -> u32 { + log!("sys_panic: called"); + // Legacy path: treat args as [ptr, len] when a0/a1 aren't forwarded. + sys_panic_with_message(args[0], args[1]) +} + +#[inline(never)] +fn halt() -> ! { + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs new file mode 100644 index 0000000..8f60b46 --- /dev/null +++ b/crates/kernel/src/task/mod.rs @@ -0,0 +1,105 @@ +#![allow(dead_code)] + +// Program launch flow (kernel side) +// --------------------------------- +// Goals: +// - Create a fresh address space for each program call (new root PPN + ASID). +// - Map a fixed, contiguous user window starting at VA 0x0 that holds: +// * Code/rodata (program bytes copied starting at VA 0x0; entry at `entry_off`) +// * A user stack (STACK_BYTES) +// * A user heap (HEAP_BYTES) with input placed at INPUT_BASE_ADDR +// - Copy call arguments (to/from addresses + input buffer) into that window. +// - Prepare a trapframe with PC/SP/args and transfer control to user code. +// +// Key pieces: +// - PROGRAM_WINDOW_BYTES covers code + rodata + stack + heap: a single map call per program. +// - TRAMPOLINE_VA is one page immediately after the user window, mapped into both +// the kernel root and the new user root. It contains two instructions: +// csrw satp, t0 +// jr t1 +// This lets us switch satp safely from a VA that stays valid across the root change. +// +// prep_program_task(to, from, code, input, entry_off): +// 1) Allocate ASID and a fresh root PPN; map the user window with user_rwx perms. +// 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). +// 3) Map the trampoline page into the user root and mirror the same physical page +// into the current kernel root; write TRAMPOLINE_CODE into it. +// 4) Build a Task with AddressSpace {root_ppn, asid} and set trapframe: +// pc = PROGRAM_VA_BASE + entry_off +// sp = top of user stack within the window +// a0..a3 = to/from/input_base/input_len +// Caller can push the task into TASKS for bookkeeping. +// +// run_task(task): +// - Save the current kernel frame (sp/ra/pc) into TASKS[0] for a future return path. +// - Preload t0 with the task root (satp value) and t1 with the user PC; load +// user sp and a0..a3; clear ra. +// - jr TRAMPOLINE_VA. The trampoline executes under the old root, writes satp +// to the new root, and immediately jr t1 into user code. There is no return +// path yet; this is a one-way handoff. +// +// Notes: +// - The window and trampoline VAs are low for simplicity; nothing here relocates. +// - We currently do not touch sstatus/mstatus or perform sfence.vma; add those +// when modeling fuller privilege transitions. + +use crate::Config; +use crate::global::NEXT_ASID; +use types::ADDRESS_LEN; + +pub mod task; +pub mod prep; +pub mod run; + +pub use task::{AddressSpace, Task, TrapFrame}; +pub use prep::prep_program_task; +pub use run::run_task; + +const PAGE_SIZE: usize = 4096; +const STACK_BYTES: usize = 0x4000; // 16 KiB user stack +const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap +pub const PROGRAM_VA_BASE: u32 = 0x0; +// Location of the page that hosts the satp-switch trampoline. Kept just past +// the user window so it does not collide with program text/stack/heap. This VA +// is mapped into both roots so the satp write does not invalidate the +// instruction stream mid-flight. +const TRAMPOLINE_VA: u32 = (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES) as u32; +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} + +/// Total mapped window for a program: code/rodata, stack, and heap. +pub const PROGRAM_WINDOW_BYTES: usize = align_up( + Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, + PAGE_SIZE, +); + +const REG_SP: usize = 2; +const REG_RA: usize = 1; +const REG_A0: usize = 10; +const REG_A1: usize = 11; +const REG_A2: usize = 12; +const REG_A3: usize = 13; +// Raw RISC-V words for the trampoline used to switch satp safely while +// executing from a page mapped in both the kernel and user roots. The kernel +// loads t0 = target satp and t1 = user PC before entering this stub so we can +// change roots and immediately branch to user code without returning to +// unmapped kernel text. +// t0: target satp value, t1: user PC (jump target). +const TRAMPOLINE_CODE: [u32; 2] = [ + 0x1802_9073, // csrw satp, t0 + 0x0003_0067, // jr t1 +]; + +const TO_PTR_ADDR: u32 = 0x120; +const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; +const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; + +pub(super) fn alloc_asid() -> u16 { + unsafe { + let counter = NEXT_ASID.get_mut(); + let asid = if *counter == 0 { 1 } else { *counter }; + *counter = asid.wrapping_add(1); + asid + } +} diff --git a/crates/kernel/src/launch.rs b/crates/kernel/src/task/prep.rs similarity index 51% rename from crates/kernel/src/launch.rs rename to crates/kernel/src/task/prep.rs index e19527e..7f48843 100644 --- a/crates/kernel/src/launch.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,93 +1,12 @@ -#![allow(dead_code)] - -// Program launch flow (kernel side) -// --------------------------------- -// Goals: -// - Create a fresh address space for each program call (new root PPN + ASID). -// - Map a fixed, contiguous user window starting at VA 0x0 that holds: -// * Code/rodata (program bytes copied starting at VA 0x0; entry at `entry_off`) -// * A user stack (STACK_BYTES) -// * A user heap (HEAP_BYTES) with input placed at INPUT_BASE_ADDR -// - Copy call arguments (to/from addresses + input buffer) into that window. -// - Prepare a trapframe with PC/SP/args and transfer control to user code. -// -// Key pieces: -// - PROGRAM_WINDOW_BYTES covers code + rodata + stack + heap: a single map call per program. -// - TRAMPOLINE_VA is one page immediately after the user window, mapped into both -// the kernel root and the new user root. It contains two instructions: -// csrw satp, t0 -// jr t1 -// This lets us switch satp safely from a VA that stays valid across the root change. -// -// prep_program_task(to, from, code, input, entry_off): -// 1) Allocate ASID and a fresh root PPN; map the user window with user_rwx perms. -// 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). -// 3) Map the trampoline page into the user root and mirror the same physical page -// into the current kernel root; write TRAMPOLINE_CODE into it. -// 4) Build a Task with AddressSpace {root_ppn, asid} and set trapframe: -// pc = PROGRAM_VA_BASE + entry_off -// sp = top of user stack within the window -// a0..a3 = to/from/input_base/input_len -// Caller can push the task into TASKS for bookkeeping. -// -// run_task(task): -// - Save the current kernel frame (sp/ra/pc) into TASKS[0] for a future return path. -// - Preload t0 with the task root (satp value) and t1 with the user PC; load -// user sp and a0..a3; clear ra. -// - jr TRAMPOLINE_VA. The trampoline executes under the old root, writes satp -// to the new root, and immediately jr t1 into user code. There is no return -// path yet; this is a one-way handoff. -// -// Notes: -// - The window and trampoline VAs are low for simplicity; nothing here relocates. -// - We currently do not touch sstatus/mstatus or perform sfence.vma; add those -// when modeling fuller privilege transitions. - use crate::{AddressSpace, Config, Task, mmu}; -use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, NEXT_ASID, TASKS}; -use program::log; -use program::logf; -use types::{address::Address, ADDRESS_LEN}; - -const PAGE_SIZE: usize = 4096; -const STACK_BYTES: usize = 0x4000; // 16 KiB user stack -const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap -pub const PROGRAM_VA_BASE: u32 = 0x0; -// Location of the page that hosts the satp-switch trampoline. Kept just past -// the user window so it does not collide with program text/stack/heap. This VA -// is mapped into both roots so the satp write does not invalidate the -// instruction stream mid-flight. -const TRAMPOLINE_VA: u32 = (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES) as u32; -const fn align_up(val: usize, align: usize) -> usize { - (val + (align - 1)) & !(align - 1) -} - -/// Total mapped window for a program: code/rodata, stack, and heap. -pub const PROGRAM_WINDOW_BYTES: usize = align_up( - Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, - PAGE_SIZE, -); - -const REG_SP: usize = 2; -const REG_RA: usize = 1; -const REG_A0: usize = 10; -const REG_A1: usize = 11; -const REG_A2: usize = 12; -const REG_A3: usize = 13; -// Raw RISC-V words for the trampoline used to switch satp safely while -// executing from a page mapped in both the kernel and user roots. The kernel -// loads t0 = target satp and t1 = user PC before entering this stub so we can -// change roots and immediately branch to user code without returning to -// unmapped kernel text. -// t0: target satp value, t1: user PC (jump target). -const TRAMPOLINE_CODE: [u32; 2] = [ - 0x1802_9073, // csrw satp, t0 - 0x0003_0067, // jr t1 -]; +use program::{log, logf}; +use types::address::Address; -const TO_PTR_ADDR: u32 = 0x120; -const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; -const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; +use super::{ + alloc_asid, FROM_PTR_ADDR, INPUT_BASE_ADDR, PAGE_SIZE, PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES, REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, + TO_PTR_ADDR, TRAMPOLINE_CODE, TRAMPOLINE_VA, +}; /// Create a new task for a program and map its virtual address window via syscalls. /// @@ -303,102 +222,3 @@ pub fn prep_program_task( Some(task) } - -/// One-way context switch into a user task: -/// - Saves the current kernel frame into TASKS[0] -/// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) -pub fn run_task(task_idx: usize) { - let (target_root, asid, pc, sp, a0, a1, a2, a3) = unsafe { - let tasks = TASKS.get_mut(); - let task = match tasks.get(task_idx) { - Some(task) => task, - None => { - logf!("run_task: invalid task slot %d", task_idx as u32); - return; - } - }; - ( - task.addr_space.root_ppn, - task.addr_space.asid, - task.tf.pc, - task.tf.regs[REG_SP], - task.tf.regs[REG_A0], - task.tf.regs[REG_A1], - task.tf.regs[REG_A2], - task.tf.regs[REG_A3], - ) - }; - unsafe { - *CURRENT_TASK.get_mut() = task_idx; - } - let kernel_root = mmu::current_root(); - logf!( - "run_task: switching satp 0x%x -> 0x%x asid=%d pc=0x%x sp=0x%x", - kernel_root, - target_root, - asid as u32, - pc, - sp, - ); - // Save the current kernel frame (SP/RA/PC) into the kernel task slot (index 0). - let mut saved_sp: u32; - let mut saved_ra: u32; - let mut saved_pc: u32; - unsafe { - core::arch::asm!("mv {out}, sp", out = out(reg) saved_sp); - core::arch::asm!("mv {out}, ra", out = out(reg) saved_ra); - core::arch::asm!("auipc {out}, 0", out = out(reg) saved_pc); - } - logf!( - "run_task: saved kernel frame sp=0x%x ra=0x%x pc=0x%x", - saved_sp, - saved_ra, - saved_pc - ); - // Stash the kernel context so a future return path could restore it. - unsafe { - let tasks = TASKS.get_mut(); - if let Some(kernel_task) = tasks.get_mut(KERNEL_TASK_SLOT) { - kernel_task.addr_space.root_ppn = kernel_root; - kernel_task.tf.regs[REG_SP] = saved_sp; - kernel_task.tf.regs[REG_RA] = saved_ra; - kernel_task.tf.pc = saved_pc; - } - } - // Update the helper's view of the current root before switching. - mmu::set_current_root(target_root); - // Set up registers and jump to the shared trampoline page (mapped in both - // the kernel and user roots). The trampoline will write satp and transfer - // control to the user PC. - unsafe { - core::arch::asm!( - "mv t0, {satp}", // satp to write - "mv t1, {pc}", // user PC - "mv ra, zero", - "mv sp, {sp}", - "mv a0, {a0}", - "mv a1, {a1}", - "mv a2, {a2}", - "mv a3, {a3}", - "jr {tramp}", - satp = in(reg) target_root, - pc = in(reg) pc, - sp = in(reg) sp, - a0 = in(reg) a0, - a1 = in(reg) a1, - a2 = in(reg) a2, - a3 = in(reg) a3, - tramp = in(reg) TRAMPOLINE_VA, - options(noreturn) - ); - } -} - -fn alloc_asid() -> u16 { - unsafe { - let counter = NEXT_ASID.get_mut(); - let asid = if *counter == 0 { 1 } else { *counter }; - *counter = asid.wrapping_add(1); - asid - } -} diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs new file mode 100644 index 0000000..9b9d308 --- /dev/null +++ b/crates/kernel/src/task/run.rs @@ -0,0 +1,95 @@ +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; +use crate::mmu; +use program::logf; + +use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_RA, REG_SP, TRAMPOLINE_VA}; + +/// One-way context switch into a user task: +/// - Saves the current kernel frame into TASKS[0] +/// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) +pub fn run_task(task_idx: usize) { + let (target_root, asid, pc, sp, a0, a1, a2, a3) = unsafe { + let tasks = TASKS.get_mut(); + let task = match tasks.get(task_idx) { + Some(task) => task, + None => { + logf!("run_task: invalid task slot %d", task_idx as u32); + return; + } + }; + ( + task.addr_space.root_ppn, + task.addr_space.asid, + task.tf.pc, + task.tf.regs[REG_SP], + task.tf.regs[REG_A0], + task.tf.regs[REG_A1], + task.tf.regs[REG_A2], + task.tf.regs[REG_A3], + ) + }; + unsafe { + *CURRENT_TASK.get_mut() = task_idx; + } + let kernel_root = mmu::current_root(); + logf!( + "run_task: switching satp 0x%x -> 0x%x asid=%d pc=0x%x sp=0x%x", + kernel_root, + target_root, + asid as u32, + pc, + sp, + ); + // Save the current kernel frame (SP/RA/PC) into the kernel task slot (index 0). + let mut saved_sp: u32; + let mut saved_ra: u32; + let mut saved_pc: u32; + unsafe { + core::arch::asm!("mv {out}, sp", out = out(reg) saved_sp); + core::arch::asm!("mv {out}, ra", out = out(reg) saved_ra); + core::arch::asm!("auipc {out}, 0", out = out(reg) saved_pc); + } + logf!( + "run_task: saved kernel frame sp=0x%x ra=0x%x pc=0x%x", + saved_sp, + saved_ra, + saved_pc + ); + // Stash the kernel context so a future return path could restore it. + unsafe { + let tasks = TASKS.get_mut(); + if let Some(kernel_task) = tasks.get_mut(KERNEL_TASK_SLOT) { + kernel_task.addr_space.root_ppn = kernel_root; + kernel_task.tf.regs[REG_SP] = saved_sp; + kernel_task.tf.regs[REG_RA] = saved_ra; + kernel_task.tf.pc = saved_pc; + } + } + // Update the helper's view of the current root before switching. + mmu::set_current_root(target_root); + // Set up registers and jump to the shared trampoline page (mapped in both + // the kernel and user roots). The trampoline will write satp and transfer + // control to the user PC. + unsafe { + core::arch::asm!( + "mv t0, {satp}", // satp to write + "mv t1, {pc}", // user PC + "mv ra, zero", + "mv sp, {sp}", + "mv a0, {a0}", + "mv a1, {a1}", + "mv a2, {a2}", + "mv a3, {a3}", + "jr {tramp}", + satp = in(reg) target_root, + pc = in(reg) pc, + sp = in(reg) sp, + a0 = in(reg) a0, + a1 = in(reg) a1, + a2 = in(reg) a2, + a3 = in(reg) a3, + tramp = in(reg) TRAMPOLINE_VA, + options(noreturn) + ); + } +} diff --git a/crates/kernel/src/task.rs b/crates/kernel/src/task/task.rs similarity index 100% rename from crates/kernel/src/task.rs rename to crates/kernel/src/task/task.rs diff --git a/crates/program/src/panic.rs b/crates/program/src/panic.rs index 9042cb6..fd8a892 100644 --- a/crates/program/src/panic.rs +++ b/crates/program/src/panic.rs @@ -8,8 +8,8 @@ pub fn vm_panic(msg: &[u8]) -> ! { core::arch::asm!( "li a7, 3", // SYSCALL_PANIC "ecall", - in("a0") msg.as_ptr(), - in("a1") msg.len(), + in("a1") msg.as_ptr(), + in("a2") msg.len(), options(noreturn), ); } From 9c51f93c4b97daa5b4688a67e01f4c7473f7427f Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 20:42:19 +0200 Subject: [PATCH 34/70] refactored how kernel and user programs map --- crates/bootloader/src/bootloader.rs | 25 ++++++++++--------------- crates/kernel/src/init.rs | 7 ++++++- crates/kernel/src/syscall/alloc.rs | 22 +++++++++++++++++----- crates/kernel/src/syscall/panic.rs | 1 - crates/kernel/src/task/mod.rs | 2 +- crates/kernel/src/task/prep.rs | 28 ++++++++++------------------ crates/kernel/src/task/task.rs | 17 +++++++++++++---- crates/types/src/boot.rs | 8 ++++++++ 8 files changed, 65 insertions(+), 45 deletions(-) diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 5957d41..f7ae54b 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -15,8 +15,6 @@ use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, Virt use vm::registers::Register; use vm::vm::VM; -const MIN_KERNEL_MAP_BYTES: usize = 16 * 1024; -const KERNEL_STACK_BYTES: usize = 4 * PAGE_SIZE; const KERNEL_WINDOW_BYTES: usize = 256 * 1024; const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; @@ -86,13 +84,6 @@ impl Bootloader { image_end = core::cmp::max(image_end, bss_end); } let image_size = image_end.checked_sub(min_base).expect("invalid image size"); - let kernel_map_bytes = core::cmp::max(image_size, MIN_KERNEL_MAP_BYTES); - let stack_base = (KERNEL_STACK_TOP as usize).saturating_sub(KERNEL_STACK_BYTES); - - assert!( - kernel_map_bytes <= stack_base, - "kernel image overlaps stack window" - ); assert!( image_end <= self.memory.size(), @@ -100,6 +91,12 @@ impl Bootloader { image_end, self.memory.size() ); + assert!( + KERNEL_WINDOW_BYTES <= self.memory.size(), + "kernel window exceeds physical memory (need {}, have {})", + KERNEL_WINDOW_BYTES, + self.memory.size() + ); // Flatten code + rodata into a single buffer and write once to set heap pointer properly. let mut image = vec![0u8; image_size]; @@ -115,16 +112,12 @@ impl Bootloader { } self.memory - .map_range(VirtualAddress(min_base as u32), kernel_map_bytes, Perms::rwx_kernel()); + .map_range(VirtualAddress(0), KERNEL_WINDOW_BYTES, Perms::rwx_kernel()); self.memory .write_bytes(VirtualAddress(min_base as u32), &image); - // Start the heap after the loaded image to avoid overwriting kernel text/rodata + // Start the heap after the loaded image to avoid overwriting kernel text/rodata. let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; self.set_next_heap(heap_start); - // Ensure the kernel has a mapped stack region near the top of memory. - let stack_base = (KERNEL_STACK_TOP as usize).saturating_sub(KERNEL_STACK_BYTES); - self.memory - .map_range(VirtualAddress(stack_base as u32), KERNEL_STACK_BYTES, Perms::rw_kernel()); // Map a direct window over all physical memory so the kernel can touch // page tables after paging is enabled. let mapped = self.memory.map_physical_range( @@ -207,6 +200,8 @@ impl Bootloader { next_heap, self.memory.size() as u32, self.memory.next_free_ppn() as u32, + 0, + KERNEL_WINDOW_BYTES as u32, ); let bytes = unsafe { slice::from_raw_parts( diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index 35fbaed..9e0f230 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -50,7 +50,12 @@ fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { *PAGE_ALLOC_INIT.get_mut() = true; } } - let task = Task::kernel(info.root_ppn, info.heap_ptr); + let task = Task::kernel( + info.root_ppn, + info.heap_ptr, + info.va_base, + info.va_len, + ); unsafe { let tasks_slot = TASKS.get_mut(); if tasks_slot.set_at(KERNEL_TASK_SLOT, task).is_err() { diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs index 113feec..6a93232 100644 --- a/crates/kernel/src/syscall/alloc.rs +++ b/crates/kernel/src/syscall/alloc.rs @@ -1,8 +1,6 @@ use program::{log, logf}; use crate::global::{CURRENT_TASK, TASKS}; -use crate::mmu; - pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { let size = args[0]; let align = args[1]; @@ -25,6 +23,13 @@ pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { return 0; } }; + logf!( + "sys_alloc: size=0x%x align=0x%x heap_ptr=0x%x task=%d", + size, + align, + task.heap_ptr, + current as u32 + ); let mask = align - 1; let start = match task.heap_ptr.checked_add(mask) { @@ -42,9 +47,16 @@ pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { } }; - let len = end.saturating_sub(start) as usize; - if !mmu::map_kernel_range(start, len, mmu::PagePerms::kernel_rw()) { - log!("sys_alloc: failed to map heap range"); + let window_base = task.addr_space.va_base; + let window_limit = window_base.saturating_add(task.addr_space.va_len); + if start < window_base || end > window_limit { + logf!( + "sys_alloc: heap range exceeds task window start=0x%x end=0x%x window=[0x%x,0x%x)", + start, + end, + window_base, + window_limit + ); return 0; } task.heap_ptr = end; diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs index eb5358f..c864db5 100644 --- a/crates/kernel/src/syscall/panic.rs +++ b/crates/kernel/src/syscall/panic.rs @@ -63,7 +63,6 @@ pub(crate) fn sys_panic_with_message(msg_ptr: u32, msg_len: u32) -> u32 { } pub(crate) fn sys_panic(args: [u32; 6]) -> u32 { - log!("sys_panic: called"); // Legacy path: treat args as [ptr, len] when a0/a1 aren't forwarded. sys_panic_with_message(args[0], args[1]) } diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 8f60b46..425f185 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -57,7 +57,7 @@ pub use run::run_task; const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack -const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap +pub const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap pub const PROGRAM_VA_BASE: u32 = 0x0; // Location of the page that hosts the satp-switch trampoline. Kept just past // the user window so it does not collide with program text/stack/heap. This VA diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 7f48843..38393d3 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -3,7 +3,7 @@ use program::{log, logf}; use types::address::Address; use super::{ - alloc_asid, FROM_PTR_ADDR, INPUT_BASE_ADDR, PAGE_SIZE, PROGRAM_VA_BASE, + alloc_asid, FROM_PTR_ADDR, HEAP_BYTES, INPUT_BASE_ADDR, PAGE_SIZE, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, TO_PTR_ADDR, TRAMPOLINE_CODE, TRAMPOLINE_VA, }; @@ -28,22 +28,6 @@ pub fn prep_program_task( return None; } - // Make sure the kernel page allocator does not hand out frames that the - // kernel heap already consumed (account code lives there). We conservatively - // push the allocator toward the top of memory so new user roots/tables - // don't overlap heap-backed data. - let total_ppn = mmu::total_ppn().unwrap_or(0); - let reserve = (PROGRAM_WINDOW_BYTES / PAGE_SIZE) as u32 + 4; // user window + a few tables - if total_ppn > reserve { - let min_ppn = total_ppn - reserve; - mmu::bump_page_allocator(min_ppn); - logf!( - "prep_program_task: bump page alloc to ppn=0x%x (total_ppn=0x%x)", - min_ppn, - total_ppn - ); - } - let asid = alloc_asid(); let root_ppn = match mmu::alloc_root() { Some(ppn) => ppn, @@ -192,7 +176,15 @@ pub fn prep_program_task( tramp_phys_2 as u32 ); - let mut task = Task::new(AddressSpace::new(root_ppn, asid), Config::HEAP_START_ADDR as u32); + let mut task = Task::new( + AddressSpace::new( + root_ppn, + asid, + PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES as u32, + ), + Config::HEAP_START_ADDR as u32, + ); // Set up initial trapframe. let stack_top = PROGRAM_VA_BASE .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); diff --git a/crates/kernel/src/task/task.rs b/crates/kernel/src/task/task.rs index 3165741..696d8e5 100644 --- a/crates/kernel/src/task/task.rs +++ b/crates/kernel/src/task/task.rs @@ -26,11 +26,20 @@ pub struct AddressSpace { pub root_ppn: u32, /// Optional address-space identifier (ASID); zero if unused. pub asid: u16, + /// Base virtual address for this address space's mapped window. + pub va_base: u32, + /// Size in bytes of the mapped virtual window. + pub va_len: u32, } impl AddressSpace { - pub fn new(root_ppn: u32, asid: u16) -> Self { - Self { root_ppn, asid } + pub fn new(root_ppn: u32, asid: u16, va_base: u32, va_len: u32) -> Self { + Self { + root_ppn, + asid, + va_base, + va_len, + } } } @@ -57,7 +66,7 @@ impl Task { /// Create the initial kernel task. This represents the supervisor itself: /// - `root_ppn` is the kernel page-table root PPN that will be loaded into satp. - pub fn kernel(root_ppn: u32, heap_ptr: u32) -> Self { - Task::new(AddressSpace::new(root_ppn, 0), heap_ptr) + pub fn kernel(root_ppn: u32, heap_ptr: u32, va_base: u32, va_len: u32) -> Self { + Task::new(AddressSpace::new(root_ppn, 0, va_base, va_len), heap_ptr) } } diff --git a/crates/types/src/boot.rs b/crates/types/src/boot.rs index 0287227..3e8028c 100644 --- a/crates/types/src/boot.rs +++ b/crates/types/src/boot.rs @@ -20,6 +20,10 @@ pub struct BootInfo { pub memory_size: u32, /// First free physical page number after bootloader allocations. pub next_free_ppn: u32, + /// Base virtual address of the mapped VA window. + pub va_base: u32, + /// Size in bytes of the mapped VA window. + pub va_len: u32, } impl BootInfo { @@ -29,6 +33,8 @@ impl BootInfo { heap_ptr: u32, memory_size: u32, next_free_ppn: u32, + va_base: u32, + va_len: u32, ) -> Self { Self { root_ppn, @@ -36,6 +42,8 @@ impl BootInfo { heap_ptr, memory_size, next_free_ppn, + va_base, + va_len, } } } From 7f6fbd27db8595120b5ccc2fbfe11d47dae7e1ad Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 21:06:29 +0200 Subject: [PATCH 35/70] Improve VM diagnostics and memory defaults - increase example test runner VM memory to 4 MiB - add physical-memory precheck in user mapping and expose allocator remaining pages - switch program-call logging to hex addresses without allocation - panic on user window mapping failures for clearer crash traces - improve guest panic formatting and avoid duplicate panic logs --- crates/examples/tests/common/test_runner.rs | 2 +- crates/kernel/src/mmu.rs | 32 +++++++++++++++++++++ crates/kernel/src/program_call.rs | 20 +++++++------ crates/kernel/src/syscall/alloc.rs | 7 ----- crates/kernel/src/syscall/panic.rs | 5 ---- crates/kernel/src/task/prep.rs | 3 +- crates/program/src/panic.rs | 20 +++++++++---- crates/vm/src/cpu.rs | 17 ----------- 8 files changed, 61 insertions(+), 45 deletions(-) diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index 5d19a1f..30a63c2 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -82,7 +82,7 @@ impl TestRunner { TestRunner { writer, verbose: false, - vm_memory_size: 512 * 1024, // larger default to accommodate bigger binaries without RVC + vm_memory_size: 4 * 1024 * 1024, // larger default to accommodate bigger binaries without RVC kernel_bytes: Self::load_kernel_from_env(), kernel_path: env::var("KERNEL_ELF").ok(), } diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/mmu.rs index 350071a..0232c0e 100644 --- a/crates/kernel/src/mmu.rs +++ b/crates/kernel/src/mmu.rs @@ -59,6 +59,14 @@ impl PageAllocator { pub fn limit_ppn(&self) -> u32 { self.limit_ppn } + + pub fn next_ppn(&self) -> u32 { + self.next_ppn + } + + pub fn remaining_ppn(&self) -> u32 { + self.limit_ppn.saturating_sub(self.next_ppn) + } } /// Return the kernel's current root PPN (satp PPN field). @@ -110,9 +118,33 @@ pub fn total_ppn() -> Option { /// Map a user-visible virtual range with the provided permissions into a specific root. pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { + if len == 0 { + return true; + } let alloc = unsafe { PAGE_ALLOC.get_mut() }; match alloc { Some(alloc) => { + let page_size = PAGE_SIZE; + let start = align_down_local(va_start as usize, page_size); + let end = match (va_start as usize).checked_add(len) { + Some(v) => align_up_local(v, page_size), + None => return false, + }; + let page_count = (end - start) / page_size; + let vpn1_start = ((start as u32) >> 22) & SV32_VPN_MASK; + let vpn1_end = (((end - 1) as u32) >> 22) & SV32_VPN_MASK; + let l2_tables = vpn1_end + .checked_sub(vpn1_start) + .map(|v| v as usize + 1) + .unwrap_or(0); + let needed = page_count + l2_tables; + let available = alloc.remaining_ppn() as usize; + if needed > available { + panic!( + "map_user_range_for_root: out of physical memory (need {} pages, have {})", + needed, available + ); + } let mapper = KernelMapper::new(alloc); map_allocating(&mapper, root_ppn, va_start, len, perms) } diff --git a/crates/kernel/src/program_call.rs b/crates/kernel/src/program_call.rs index ee2c661..dfefa53 100644 --- a/crates/kernel/src/program_call.rs +++ b/crates/kernel/src/program_call.rs @@ -3,6 +3,7 @@ use alloc::format; use kernel::{prep_program_task, run_task, Config, PROGRAM_WINDOW_BYTES}; use kernel::global::{STATE, TASKS}; use program::{log, logf}; +use program::parser::HexCodec; use state::State; use types::transaction::Transaction; @@ -56,15 +57,18 @@ pub(crate) fn program_call(tx: &Transaction) { ); } + let mut from_buf = [0u8; 40]; + let mut to_buf = [0u8; 40]; + let from_hex = HexCodec::encode(tx.from.as_ref(), &mut from_buf); + let to_hex = HexCodec::encode(tx.to.as_ref(), &mut to_buf); logf!( - "%s", - display: format!( - "Program call: from={} to={} input_len={} code_len={}", - tx.from, - tx.to, - tx.data.len(), - code_len - ) + "Program call: from=%s to=%s input_len=%d code_len=%d", + from_hex.as_ptr() as u32, + from_hex.len() as u32, + to_hex.as_ptr() as u32, + to_hex.len() as u32, + tx.data.len() as u32, + code_len as u32 ); let entry_off = first_nz as u32; diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs index 6a93232..1f5b626 100644 --- a/crates/kernel/src/syscall/alloc.rs +++ b/crates/kernel/src/syscall/alloc.rs @@ -23,13 +23,6 @@ pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { return 0; } }; - logf!( - "sys_alloc: size=0x%x align=0x%x heap_ptr=0x%x task=%d", - size, - align, - task.heap_ptr, - current as u32 - ); let mask = align - 1; let start = match task.heap_ptr.checked_add(mask) { diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs index c864db5..9ea10db 100644 --- a/crates/kernel/src/syscall/panic.rs +++ b/crates/kernel/src/syscall/panic.rs @@ -49,11 +49,6 @@ pub(crate) fn sys_panic_with_message(msg_ptr: u32, msg_len: u32) -> u32 { } let msg = &buf[..dst_off]; - logf!( - "sys_panic: %s", - msg.as_ptr() as u32, - msg.len() as u32 - ); if let Ok(s) = core::str::from_utf8(msg) { logf!("guest panic: %s", s.as_ptr() as u32, s.len() as u32); } else { diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 38393d3..e861484 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -54,8 +54,7 @@ pub fn prep_program_task( ); let perms = mmu::PagePerms::user_rwx(); if !mmu::map_user_range_for_root(root_ppn, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { - logf!("launch_program: mapping failed (root=0x%x)", root_ppn); - return None; + panic!("launch_program: mapping failed (root=0x{:x})", root_ppn); } // Copy the full program image starting at VA 0 so section offsets (e.g. .text at 0x400) diff --git a/crates/program/src/panic.rs b/crates/program/src/panic.rs index fd8a892..663703c 100644 --- a/crates/program/src/panic.rs +++ b/crates/program/src/panic.rs @@ -27,10 +27,20 @@ pub fn vm_panic(msg: &[u8]) -> ! { #[cfg(all(target_arch = "riscv32", feature = "guest_handlers"))] #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { - let msg_bytes = if let Some(s) = info.message().as_str() { - s.as_bytes() - } else { - b"guest panic" + use core::fmt::Write; + + let mut buf = [0u8; 256]; + let len = { + let mut writer = crate::BufferWriter::new(&mut buf); + if write!(&mut writer, "{}", info).is_ok() { + writer.len() + } else { + 0 + } }; - vm_panic(msg_bytes); + if len == 0 { + vm_panic(b"guest panic"); + } else { + vm_panic(&buf[..len]); + } } diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 91b51d0..5f74c5d 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -206,23 +206,6 @@ impl CPU { Some(val) => val & !0x3, None => return false, }; - if let Some(syscall_id) = syscall_id { - self.log( - &format!( - "trap_to_vector: pc=0x{:08x} -> stvec=0x{:08x} cause=0x{:x} stval=0x{:x} syscall=0x{:x}", - self.pc, stvec, cause, trap_value, syscall_id - ), - false, - ); - } else { - self.log( - &format!( - "trap_to_vector: pc=0x{:08x} -> stvec=0x{:08x} cause=0x{:x} stval=0x{:x}", - self.pc, stvec, cause, trap_value - ), - false, - ); - } self.set_pc(stvec) } From 8fde851af715e44baf75ac89309af034602a612d Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 21:23:08 +0200 Subject: [PATCH 36/70] task run initiates --- crates/kernel/src/task/prep.rs | 8 +------- crates/kernel/src/task/run.rs | 26 ++++++++++---------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index e861484..862d9a7 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -149,13 +149,7 @@ pub fn prep_program_task( return None; } }; - if !mmu::map_physical_range_for_root( - mmu::current_root(), - TRAMPOLINE_VA, - tramp_phys, - PAGE_SIZE, - tramp_perms, - ) { + if !mmu::mirror_user_range_into_kernel(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { log!("prep_program_task: failed to mirror trampoline into kernel root"); return None; } diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index 9b9d308..4f9f44f 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -72,23 +72,17 @@ pub fn run_task(task_idx: usize) { // control to the user PC. unsafe { core::arch::asm!( - "mv t0, {satp}", // satp to write - "mv t1, {pc}", // user PC "mv ra, zero", - "mv sp, {sp}", - "mv a0, {a0}", - "mv a1, {a1}", - "mv a2, {a2}", - "mv a3, {a3}", - "jr {tramp}", - satp = in(reg) target_root, - pc = in(reg) pc, - sp = in(reg) sp, - a0 = in(reg) a0, - a1 = in(reg) a1, - a2 = in(reg) a2, - a3 = in(reg) a3, - tramp = in(reg) TRAMPOLINE_VA, + "mv sp, t2", + "jr t3", + in("t0") target_root, + in("t1") pc, + in("a0") a0, + in("a1") a1, + in("a2") a2, + in("a3") a3, + in("t2") sp, + in("t3") TRAMPOLINE_VA, options(noreturn) ); } From 35adecf0ff9b37dad132bf70bb819345042f6f54 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 27 Dec 2025 21:51:16 +0200 Subject: [PATCH 37/70] Add minimal privilege-mode transitions and log tagging - track U/S privilege mode in VM and propagate caller mode into syscalls - switch user entry to sret trampoline and set sepc/sstatus.SPP - tag syscall logs as Kernel or Guest with distinct emoji - update syscall handler signatures and tests for caller mode --- crates/bootloader/src/syscalls.rs | 19 +++++-- crates/bootloader/tests/allocator_test.rs | 5 ++ crates/kernel/src/syscall/mod.rs | 18 ++++-- crates/kernel/src/task/mod.rs | 17 +++--- crates/kernel/src/task/prep.rs | 69 ++++------------------- crates/kernel/src/task/run.rs | 13 ++++- crates/kernel/src/trap.rs | 26 +++++---- crates/vm/src/cpu.rs | 44 +++++++++++++++ crates/vm/src/exe.rs | 5 +- crates/vm/src/sys_call.rs | 2 + 10 files changed, 132 insertions(+), 86 deletions(-) diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index 0c127ef..4fd03c8 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -124,6 +124,7 @@ impl SyscallHandler for DefaultSyscallHandler { &mut self, call_id: u32, args: [u32; 6], + caller_mode: vm::cpu::PrivilegeMode, memory: Memory, host: &mut Box, regs: &mut [u32; 32], @@ -136,7 +137,7 @@ impl SyscallHandler for DefaultSyscallHandler { SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, metering), SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, metering), SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), - SYSCALL_LOG => self.sys_log(args, memory, metering), + SYSCALL_LOG => self.sys_log(args, caller_mode, memory, metering), SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host, metering), SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host, metering), SYSCALL_ALLOC => self.sys_alloc(args, memory, metering), @@ -475,7 +476,13 @@ impl DefaultSyscallHandler { panic!("🔥 Guest panic: {}", msg); } - fn sys_log(&mut self, args: [u32; 6], memory: Memory, metering: &mut dyn Metering) -> u32 { + fn sys_log( + &mut self, + args: [u32; 6], + caller_mode: vm::cpu::PrivilegeMode, + memory: Memory, + metering: &mut dyn Metering, + ) -> u32 { let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; let payload_len = fmt_len.saturating_add(arg_len) as usize; if matches!( @@ -668,12 +675,16 @@ impl DefaultSyscallHandler { output.push(c); } } + let prefix = match caller_mode { + vm::cpu::PrivilegeMode::Supervisor => "🛡️ Kernel", + vm::cpu::PrivilegeMode::User => "📜 Guest", + }; match &self.verbose_writer { Some(writer) => { - let _ = writeln!(writer.borrow_mut(), "📜 Guest Log: {}", output); + let _ = writeln!(writer.borrow_mut(), "{}: {}", prefix, output); } None => { - println!("📜 Guest Log: {}", output); + println!("{}: {}", prefix, output); } } 0 diff --git a/crates/bootloader/tests/allocator_test.rs b/crates/bootloader/tests/allocator_test.rs index b3531c6..e892196 100644 --- a/crates/bootloader/tests/allocator_test.rs +++ b/crates/bootloader/tests/allocator_test.rs @@ -21,6 +21,7 @@ fn test_allocator_syscalls() { let (result, _) = syscall_handler.handle_syscall( SYSCALL_ALLOC, args, + vm::cpu::PrivilegeMode::Supervisor, memory.clone(), &mut host, &mut regs, @@ -34,6 +35,7 @@ fn test_allocator_syscalls() { let (dealloc_result, _) = syscall_handler.handle_syscall( SYSCALL_DEALLOC, dealloc_args, + vm::cpu::PrivilegeMode::Supervisor, memory.clone(), &mut host, &mut regs, @@ -62,6 +64,7 @@ fn test_multiple_allocations() { let (ptr, _) = syscall_handler.handle_syscall( SYSCALL_ALLOC, args, + vm::cpu::PrivilegeMode::Supervisor, memory.clone(), &mut host, &mut regs, @@ -102,6 +105,7 @@ fn test_alignment_requirements() { let (ptr, _) = syscall_handler.handle_syscall( SYSCALL_ALLOC, args, + vm::cpu::PrivilegeMode::Supervisor, memory.clone(), &mut host, &mut regs, @@ -130,6 +134,7 @@ fn test_invalid_alignment() { let (ptr, _) = syscall_handler.handle_syscall( SYSCALL_ALLOC, args, + vm::cpu::PrivilegeMode::Supervisor, memory.clone(), &mut host, &mut regs, diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 08eb769..bbb95f6 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -22,12 +22,18 @@ pub const SYSCALL_TRANSFER: u32 = 9; pub const SYSCALL_BALANCE: u32 = 10; pub const SYSCALL_BRK: u32 = 214; -pub fn dispatch_syscall(call_id: u32, args: [u32; 6]) -> u32 { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CallerMode { + User, + Supervisor, +} + +pub fn dispatch_syscall(call_id: u32, args: [u32; 6], caller_mode: CallerMode) -> u32 { match call_id { SYSCALL_STORAGE_GET => sys_storage_get(args), SYSCALL_STORAGE_SET => sys_storage_set(args), SYSCALL_PANIC => sys_panic(args), - SYSCALL_LOG => sys_log(args), + SYSCALL_LOG => sys_log(args, caller_mode), SYSCALL_CALL_PROGRAM => sys_call_program(args), SYSCALL_FIRE_EVENT => sys_fire_event(args), SYSCALL_ALLOC => sys_alloc(args), @@ -52,8 +58,12 @@ fn sys_storage_set(_args: [u32; 6]) -> u32 { 0 } -fn sys_log(_args: [u32; 6]) -> u32 { - log!("sys_log: need implementation"); +fn sys_log(_args: [u32; 6], caller_mode: CallerMode) -> u32 { + if caller_mode == CallerMode::Supervisor { + log!("kernel: sys_log: need implementation"); + } else { + log!("guest: sys_log: need implementation"); + } 0 } diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 425f185..6323298 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -16,7 +16,7 @@ // - TRAMPOLINE_VA is one page immediately after the user window, mapped into both // the kernel root and the new user root. It contains two instructions: // csrw satp, t0 -// jr t1 +// sret // This lets us switch satp safely from a VA that stays valid across the root change. // // prep_program_task(to, from, code, input, entry_off): @@ -32,10 +32,10 @@ // // run_task(task): // - Save the current kernel frame (sp/ra/pc) into TASKS[0] for a future return path. -// - Preload t0 with the task root (satp value) and t1 with the user PC; load -// user sp and a0..a3; clear ra. +// - Preload t0 with the task root (satp value); load user sp and a0..a3; clear ra. +// - Set sepc to the user PC and clear sstatus.SPP so sret enters user mode. // - jr TRAMPOLINE_VA. The trampoline executes under the old root, writes satp -// to the new root, and immediately jr t1 into user code. There is no return +// to the new root, and executes sret into user code. There is no return // path yet; this is a one-way handoff. // // Notes: @@ -82,13 +82,12 @@ const REG_A2: usize = 12; const REG_A3: usize = 13; // Raw RISC-V words for the trampoline used to switch satp safely while // executing from a page mapped in both the kernel and user roots. The kernel -// loads t0 = target satp and t1 = user PC before entering this stub so we can -// change roots and immediately branch to user code without returning to -// unmapped kernel text. -// t0: target satp value, t1: user PC (jump target). +// loads t0 = target satp before entering this stub so we can change roots +// and return to user mode at sepc without returning to unmapped kernel text. +// t0: target satp value. const TRAMPOLINE_CODE: [u32; 2] = [ 0x1802_9073, // csrw satp, t0 - 0x0003_0067, // jr t1 + 0x1020_0073, // sret ]; const TO_PTR_ADDR: u32 = 0x120; diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 862d9a7..77f80a3 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -36,14 +36,7 @@ pub fn prep_program_task( return None; } }; - logf!( - "prep_program_task: new root=0x%x asid=%d code_len=%d input_len=%d entry_off=0x%x", - root_ppn, - asid as u32, - code.len() as u32, - input.len() as u32, - entry_off, - ); + let window_end = PROGRAM_VA_BASE.wrapping_add(PROGRAM_WINDOW_BYTES as u32); logf!( "launch_program: asid=%d root=0x%x map=[0x%x,0x%x)", @@ -60,12 +53,7 @@ pub fn prep_program_task( // Copy the full program image starting at VA 0 so section offsets (e.g. .text at 0x400) // land where the ELF expected them. Entry offset is provided by the caller. if entry_off as usize >= code.len() { - logf!( - "prep_program_task: entry_off 0x%x is outside code len %d", - entry_off, - code.len() as u32 - ); - return None; + panic!("launch_program: invalid entry offset"); } if code.len() >= entry_off as usize + 8 { let head = u32::from_le_bytes([ @@ -80,30 +68,15 @@ pub fn prep_program_task( code[entry_off as usize + 6], code[entry_off as usize + 7], ]); - logf!( - "prep_program_task: code head[0..8]=0x%x 0x%x (entry_off=0x%x)", - head, - head2, - entry_off, - ); } let nz_count = code.iter().filter(|&&b| b != 0).count(); let local_first_nz = code.iter().position(|&b| b != 0).unwrap_or(code.len()); - logf!( - "prep_program_task: local code stats first_nz=0x%x nz_count=%d", - local_first_nz as u32, - nz_count as u32 - ); + if !mmu::copy_into_user(root_ppn, PROGRAM_VA_BASE, code) { logf!("launch_program: failed to copy code into root=0x%x", root_ppn); return None; } - logf!( - "prep_program_task: copied code to 0x%x len=%d nz_count=%d", - PROGRAM_VA_BASE, - code.len() as u32, - nz_count as u32 - ); + if !mmu::copy_into_user(root_ppn, TO_PTR_ADDR, &to.0) { logf!("launch_program: failed to copy 'to' address into root=0x%x", root_ppn); return None; @@ -113,16 +86,11 @@ pub fn prep_program_task( return None; } if !mmu::copy_into_user(root_ppn, INPUT_BASE_ADDR, input) { - logf!("launch_program: failed to copy input into root=0x%x", root_ppn); - return None; + panic!( + "prep_program_task: failed to copy input into root=0x{:x}", + root_ppn + ); } - logf!( - "prep_program_task: copied args to=0x%x from=0x%x input=0x%x len=%d", - TO_PTR_ADDR, - FROM_PTR_ADDR, - INPUT_BASE_ADDR, - input.len() as u32 - ); // Sanity check where the code landed in the user root. let entry_va = PROGRAM_VA_BASE.wrapping_add(entry_off); @@ -139,35 +107,22 @@ pub fn prep_program_task( // satp safely before jumping into the user program. let tramp_perms = mmu::PagePerms::user_rwx(); if !mmu::map_user_range_for_root(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { - logf!("prep_program_task: failed to map trampoline page in user root"); - return None; + panic!("prep_program_task: failed to map trampoline page in user root"); } let tramp_phys = match mmu::translate_user_va(root_ppn, TRAMPOLINE_VA) { Some(p) => p as u32, - None => { - log!("prep_program_task: trampoline VA not mapped"); - return None; - } + None => panic!("prep_program_task: trampoline VA not mapped"), }; if !mmu::mirror_user_range_into_kernel(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { - log!("prep_program_task: failed to mirror trampoline into kernel root"); - return None; + panic!("prep_program_task: failed to mirror trampoline into kernel root"); } let mut tramp_bytes = [0u8; TRAMPOLINE_CODE.len() * 4]; for (i, word) in TRAMPOLINE_CODE.iter().enumerate() { tramp_bytes[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); } if !mmu::copy_into_user(root_ppn, TRAMPOLINE_VA, &tramp_bytes) { - log!("prep_program_task: failed to populate trampoline code"); - return None; + panic!("prep_program_task: failed to populate trampoline code"); } - let tramp_phys_2 = mmu::translate_user_va(root_ppn, TRAMPOLINE_VA + 4).unwrap_or(usize::MAX); - logf!( - "prep_program_task: trampoline mapped va=0x%x phys=0x%x phys(+4)=0x%x", - TRAMPOLINE_VA, - tramp_phys, - tramp_phys_2 as u32 - ); let mut task = Task::new( AddressSpace::new( diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index 4f9f44f..daeffc5 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -4,6 +4,8 @@ use program::logf; use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_RA, REG_SP, TRAMPOLINE_VA}; +const SSTATUS_SPP: u32 = 1 << 8; + /// One-way context switch into a user task: /// - Saves the current kernel frame into TASKS[0] /// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) @@ -65,6 +67,16 @@ pub fn run_task(task_idx: usize) { kernel_task.tf.pc = saved_pc; } } + // Prepare to enter user mode via sret: set sepc and clear sstatus.SPP. + let mut sstatus: u32; + unsafe { + core::arch::asm!("csrr {0}, sstatus", out(reg) sstatus); + } + sstatus &= !SSTATUS_SPP; + unsafe { + core::arch::asm!("csrw sstatus, {0}", in(reg) sstatus); + core::arch::asm!("csrw sepc, {0}", in(reg) pc); + } // Update the helper's view of the current root before switching. mmu::set_current_root(target_root); // Set up registers and jump to the shared trampoline page (mapped in both @@ -76,7 +88,6 @@ pub fn run_task(task_idx: usize) { "mv sp, t2", "jr t3", in("t0") target_root, - in("t1") pc, in("a0") a0, in("a1") a1, in("a2") a2, diff --git a/crates/kernel/src/trap.rs b/crates/kernel/src/trap.rs index 5c2e90b..cb9b4dc 100644 --- a/crates/kernel/src/trap.rs +++ b/crates/kernel/src/trap.rs @@ -4,6 +4,8 @@ use program::{log, logf}; use crate::syscall; const SCAUSE_ECALL_FROM_U: usize = 8; +const SCAUSE_ECALL_FROM_S: usize = 9; +const SSTATUS_SPP: u32 = 1 << 8; const SAVED_REG_COUNT: usize = 11; /// Install the kernel trap vector and set up the kernel stack for traps. @@ -80,14 +82,6 @@ pub extern "C" fn handle_trap(saved: *mut u32) { let sp: u32; unsafe { asm!("mv {0}, sp", out(reg) sp); } - // logf!( - // "trap_entry: scause=0x%x stval=0x%x sepc=0x%x satp=0x%x sp=0x%x", - // scause as u32, - // stval as u32, - // sepc, - // satp, - // sp - // ); let is_interrupt = (scause >> 31) != 0; if is_interrupt { panic!( @@ -98,7 +92,7 @@ pub extern "C" fn handle_trap(saved: *mut u32) { let code = scause & 0xfff; match code { - SCAUSE_ECALL_FROM_U => { + SCAUSE_ECALL_FROM_U | SCAUSE_ECALL_FROM_S => { let args = [ regs[3], // a1 regs[4], // a2 @@ -108,7 +102,12 @@ pub extern "C" fn handle_trap(saved: *mut u32) { regs[8], // a6 ]; let call_id = regs[9]; // a7 - let ret = syscall::dispatch_syscall(call_id, args); + let caller_mode = if read_sstatus() & SSTATUS_SPP != 0 { + syscall::CallerMode::Supervisor + } else { + syscall::CallerMode::User + }; + let ret = syscall::dispatch_syscall(call_id, args, caller_mode); regs[2] = ret; // a0 return value regs[0] = regs[0].wrapping_add(4); // Advance past ecall } @@ -130,6 +129,13 @@ fn read_satp() -> u32 { value } +#[inline(always)] +fn read_sstatus() -> u32 { + let value: u32; + unsafe { asm!("csrr {0}, sstatus", out(reg) value); } + value +} + #[inline(always)] fn read_stval() -> usize { let value: usize; diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 5f74c5d..bca8ebf 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -12,11 +12,20 @@ use std::rc::Rc; mod exec; pub const CSR_SATP: u16 = 0x180; +pub const CSR_SSTATUS: u16 = 0x100; pub const CSR_STVEC: u16 = 0x105; pub const CSR_SEPC: u16 = 0x141; pub const CSR_SCAUSE: u16 = 0x142; pub const CSR_STVAL: u16 = 0x143; const SCAUSE_ECALL_FROM_U: u32 = 8; +const SCAUSE_ECALL_FROM_S: u32 = 9; +const SSTATUS_SPP: u32 = 1 << 8; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrivilegeMode { + User, + Supervisor, +} /// Represents the Central Processing Unit (CPU) of our RISC-V virtual machine. /// @@ -84,6 +93,9 @@ pub struct CPU { /// Minimal CSR storage for CSR instructions pub csrs: HashMap, + + /// Current privilege mode (minimal U/S support). + pub priv_mode: PrivilegeMode, } impl std::fmt::Debug for CPU { @@ -130,6 +142,7 @@ impl CPU { verbose_writer: None, metering, csrs: HashMap::new(), + priv_mode: PrivilegeMode::Supervisor, } } @@ -175,6 +188,7 @@ impl CPU { 0xF11 | 0xF12 | 0xF13 => *self.csrs.get(&csr).unwrap_or(&0), // mvendorid/marchid/mimpid 0x301 => *self.csrs.get(&csr).unwrap_or(&0), // misa 0x300 => *self.csrs.get(&csr).unwrap_or(&0), // mstatus + CSR_SSTATUS => *self.csrs.get(&csr).unwrap_or(&0), _ => *self.csrs.get(&csr).unwrap_or(&0), }) } @@ -192,6 +206,34 @@ impl CPU { self.write_csr(CSR_SATP, value) } + fn set_sstatus_spp(&mut self, prev: PrivilegeMode) { + let mut sstatus = self.read_csr(CSR_SSTATUS).unwrap_or(0); + match prev { + PrivilegeMode::User => sstatus &= !SSTATUS_SPP, + PrivilegeMode::Supervisor => sstatus |= SSTATUS_SPP, + } + let _ = self.write_csr(CSR_SSTATUS, sstatus); + } + + fn take_sstatus_spp(&mut self) -> PrivilegeMode { + let mut sstatus = self.read_csr(CSR_SSTATUS).unwrap_or(0); + let prev = if sstatus & SSTATUS_SPP != 0 { + PrivilegeMode::Supervisor + } else { + PrivilegeMode::User + }; + sstatus &= !SSTATUS_SPP; + let _ = self.write_csr(CSR_SSTATUS, sstatus); + prev + } + + fn ecall_cause(&self) -> u32 { + match self.priv_mode { + PrivilegeMode::User => SCAUSE_ECALL_FROM_U, + PrivilegeMode::Supervisor => SCAUSE_ECALL_FROM_S, + } + } + fn trap_to_vector(&mut self, cause: u32, trap_value: u32, syscall_id: Option) -> bool { if !self.write_csr(CSR_SEPC, self.pc) { panic!("trap_to_vector: failed to write sepc"); @@ -206,6 +248,8 @@ impl CPU { Some(val) => val & !0x3, None => return false, }; + self.set_sstatus_spp(self.priv_mode); + self.priv_mode = PrivilegeMode::Supervisor; self.set_pc(stvec) } diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index b800d4e..b56fb41 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -796,7 +796,7 @@ impl CPU { if self.has_trap_vector() { // Bypass trap for logging syscalls so they execute directly. if call_id != SYSCALL_LOG { - if !self.trap_to_vector(SCAUSE_ECALL_FROM_U, 0, Some(call_id)) { + if !self.trap_to_vector(self.ecall_cause(), 0, Some(call_id)) { panic!( "trap_to_vector returned false for ecall id={} pc=0x{:08x}", call_id, self.pc @@ -808,6 +808,7 @@ impl CPU { let (result, cont) = self.syscall_handler.handle_syscall( call_id, args, + self.priv_mode, memory, host, &mut self.regs, @@ -883,9 +884,11 @@ impl CPU { Some(v) => v, None => return false, }; + let prev = self.take_sstatus_spp(); if !self.set_pc(target) { return false; } + self.priv_mode = prev; return true; } diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index 01064c0..4dd16c5 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -1,3 +1,4 @@ +use crate::cpu::PrivilegeMode; use crate::host_interface::HostInterface; use crate::memory::Memory; use crate::metering::Metering; @@ -22,6 +23,7 @@ pub trait SyscallHandler: std::fmt::Debug { &mut self, call_id: u32, args: [u32; 6], + caller_mode: PrivilegeMode, memory: Memory, host: &mut Box, regs: &mut [u32; 32], From ecfc806d2b9d7a0947f20567890e8301c6789efc Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 28 Dec 2025 17:54:54 +0200 Subject: [PATCH 38/70] Fix user-mode trap entry/return via trampolines - add a trap-entry trampoline that switches back to the kernel root before running trap_entry - restore user satp on trap return and keep supervisor returns on sret - refactor trap_entry into naked helpers for save/restore/return - document trampoline layout and usage --- crates/kernel/src/task/mod.rs | 24 ++++--- crates/kernel/src/task/prep.rs | 59 ++++++++++++++++- crates/kernel/src/task/run.rs | 3 +- crates/kernel/src/trap.rs | 115 +++++++++++++++++++++------------ 4 files changed, 147 insertions(+), 54 deletions(-) diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 6323298..ca9948e 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -14,16 +14,17 @@ // Key pieces: // - PROGRAM_WINDOW_BYTES covers code + rodata + stack + heap: a single map call per program. // - TRAMPOLINE_VA is one page immediately after the user window, mapped into both -// the kernel root and the new user root. It contains two instructions: -// csrw satp, t0 -// sret -// This lets us switch satp safely from a VA that stays valid across the root change. +// the kernel root and the new user root. It contains: +// * an entry trampoline that switches satp and sret's into user mode +// * a trap trampoline that switches satp back to the kernel root and jumps +// to the real trap_entry +// This keeps trap entry valid even when the current root is the user page table. // // prep_program_task(to, from, code, input, entry_off): // 1) Allocate ASID and a fresh root PPN; map the user window with user_rwx perms. // 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). // 3) Map the trampoline page into the user root and mirror the same physical page -// into the current kernel root; write TRAMPOLINE_CODE into it. +// into the current kernel root; write trampoline code into it. // 4) Build a Task with AddressSpace {root_ppn, asid} and set trapframe: // pc = PROGRAM_VA_BASE + entry_off // sp = top of user stack within the window @@ -34,6 +35,7 @@ // - Save the current kernel frame (sp/ra/pc) into TASKS[0] for a future return path. // - Preload t0 with the task root (satp value); load user sp and a0..a3; clear ra. // - Set sepc to the user PC and clear sstatus.SPP so sret enters user mode. +// - Set stvec to the trap trampoline VA. // - jr TRAMPOLINE_VA. The trampoline executes under the old root, writes satp // to the new root, and executes sret into user code. There is no return // path yet; this is a one-way handoff. @@ -59,11 +61,15 @@ const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack pub const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap pub const PROGRAM_VA_BASE: u32 = 0x0; -// Location of the page that hosts the satp-switch trampoline. Kept just past +// Location of the page that hosts the satp-switch trampolines. Kept just past // the user window so it does not collide with program text/stack/heap. This VA -// is mapped into both roots so the satp write does not invalidate the +// is mapped into both roots so satp can be switched without invalidating the // instruction stream mid-flight. -const TRAMPOLINE_VA: u32 = (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES) as u32; +pub const TRAMPOLINE_VA: u32 = + (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES) as u32; // Shared page just past user window. +const TRAP_TRAMPOLINE_OFFSET: usize = 0x10; // Offset for the trap-entry stub within the page. +pub const TRAP_TRAMPOLINE_VA: u32 = + TRAMPOLINE_VA + TRAP_TRAMPOLINE_OFFSET as u32; // stvec target for user-mode traps. const fn align_up(val: usize, align: usize) -> usize { (val + (align - 1)) & !(align - 1) } @@ -80,7 +86,7 @@ const REG_A0: usize = 10; const REG_A1: usize = 11; const REG_A2: usize = 12; const REG_A3: usize = 13; -// Raw RISC-V words for the trampoline used to switch satp safely while +// Raw RISC-V words for the entry trampoline used to switch satp safely while // executing from a page mapped in both the kernel and user roots. The kernel // loads t0 = target satp before entering this stub so we can change roots // and return to user mode at sepc without returning to unmapped kernel text. diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 77f80a3..32110f1 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -5,9 +5,52 @@ use types::address::Address; use super::{ alloc_asid, FROM_PTR_ADDR, HEAP_BYTES, INPUT_BASE_ADDR, PAGE_SIZE, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, - TO_PTR_ADDR, TRAMPOLINE_CODE, TRAMPOLINE_VA, + TO_PTR_ADDR, TRAMPOLINE_CODE, TRAMPOLINE_VA, TRAP_TRAMPOLINE_OFFSET, }; +const REG_T0: u32 = 5; +const REG_T1: u32 = 6; +const REG_T2: u32 = 7; +const TRAP_TRAMPOLINE_WORDS: usize = 7; // csrr + 2x(hi/lo) + csrw + jalr + +fn split_imm(val: u32) -> (u32, i32) { + // Build a LUI/ADDI pair for a full 32-bit immediate. + let hi = ((val as u64 + 0x800) >> 12) as u32; + let lo = val as i64 - ((hi as i64) << 12); + (hi, lo as i32) +} + +fn encode_lui(rd: u32, imm20: u32) -> u32 { + (imm20 << 12) | (rd << 7) | 0x37 +} + +fn encode_addi(rd: u32, rs1: u32, imm12: i32) -> u32 { + ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x13 +} + +fn encode_jalr(rd: u32, rs1: u32, imm12: i32) -> u32 { + ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x67 +} + +fn encode_csrr(rd: u32, csr: u32) -> u32 { + (csr << 20) | (0 << 15) | (0b010 << 12) | (rd << 7) | 0x73 +} + +fn build_trap_trampoline(kernel_satp: u32, trap_entry: u32) -> [u32; TRAP_TRAMPOLINE_WORDS] { + // Trampoline swaps to the kernel root and jumps to trap_entry, preserving user satp in t0. + let (satp_hi, satp_lo) = split_imm(kernel_satp); + let (entry_hi, entry_lo) = split_imm(trap_entry); + [ + encode_csrr(REG_T0, 0x180), // csrr t0, satp + encode_lui(REG_T1, satp_hi), + encode_addi(REG_T1, REG_T1, satp_lo), + 0x1803_1073, // csrw satp, t1 + encode_lui(REG_T2, entry_hi), + encode_addi(REG_T2, REG_T2, entry_lo), + encode_jalr(0, REG_T2, 0), // jr t2 + ] +} + /// Create a new task for a program and map its virtual address window via syscalls. /// /// This sets up: @@ -109,17 +152,27 @@ pub fn prep_program_task( if !mmu::map_user_range_for_root(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { panic!("prep_program_task: failed to map trampoline page in user root"); } - let tramp_phys = match mmu::translate_user_va(root_ppn, TRAMPOLINE_VA) { + let _tramp_phys = match mmu::translate_user_va(root_ppn, TRAMPOLINE_VA) { Some(p) => p as u32, None => panic!("prep_program_task: trampoline VA not mapped"), }; if !mmu::mirror_user_range_into_kernel(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { panic!("prep_program_task: failed to mirror trampoline into kernel root"); } - let mut tramp_bytes = [0u8; TRAMPOLINE_CODE.len() * 4]; + let kernel_root = mmu::current_root(); + let trap_entry = crate::trap::trap_entry as usize as u32; + let trap_trampoline = build_trap_trampoline(kernel_root, trap_entry); + // Stash both trampolines in a single shared page. + let mut tramp_bytes = + [0u8; TRAP_TRAMPOLINE_OFFSET + TRAP_TRAMPOLINE_WORDS * 4]; for (i, word) in TRAMPOLINE_CODE.iter().enumerate() { tramp_bytes[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); } + for (i, word) in trap_trampoline.iter().enumerate() { + // Trap stub lives at TRAP_TRAMPOLINE_OFFSET for stvec to target. + let base = TRAP_TRAMPOLINE_OFFSET + i * 4; + tramp_bytes[base..base + 4].copy_from_slice(&word.to_le_bytes()); + } if !mmu::copy_into_user(root_ppn, TRAMPOLINE_VA, &tramp_bytes) { panic!("prep_program_task: failed to populate trampoline code"); } diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index daeffc5..31e2ae9 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -2,7 +2,7 @@ use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; use crate::mmu; use program::logf; -use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_RA, REG_SP, TRAMPOLINE_VA}; +use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_RA, REG_SP, TRAMPOLINE_VA, TRAP_TRAMPOLINE_VA}; const SSTATUS_SPP: u32 = 1 << 8; @@ -76,6 +76,7 @@ pub fn run_task(task_idx: usize) { unsafe { core::arch::asm!("csrw sstatus, {0}", in(reg) sstatus); core::arch::asm!("csrw sepc, {0}", in(reg) pc); + core::arch::asm!("csrw stvec, {0}", in(reg) TRAP_TRAMPOLINE_VA); } // Update the helper's view of the current root before switching. mmu::set_current_root(target_root); diff --git a/crates/kernel/src/trap.rs b/crates/kernel/src/trap.rs index cb9b4dc..8571574 100644 --- a/crates/kernel/src/trap.rs +++ b/crates/kernel/src/trap.rs @@ -2,6 +2,7 @@ use core::arch::asm; use program::{log, logf}; use crate::syscall; +use crate::task::TRAMPOLINE_VA; const SCAUSE_ECALL_FROM_U: usize = 8; const SCAUSE_ECALL_FROM_S: usize = 9; @@ -21,58 +22,90 @@ pub fn init_trap_vector(kstack_top: u32) { /// Trap entry stub: /// - Switch to the kernel stack via sscratch. -/// - Save sepc, ra, a0-a7, and t0. +/// - Save sepc, ra, a0-a7, and t0 (user satp). /// - Call into the Rust trap handler with a pointer to the saved area. -/// - Restore registers and return with sret. +/// - Restore registers and return via the shared trampoline. // #[unsafe(naked)] pub unsafe extern "C" fn trap_entry() -> ! { unsafe { - asm!( - // Switch to kernel stack and make room for saved registers. - "csrrw sp, sscratch, sp", - "addi sp, sp, -44", - // Save caller-saved registers we clobber and sepc. - "sw t0, 40(sp)", - "csrr t0, sepc", - "sw t0, 0(sp)", // saved sepc - "sw ra, 4(sp)", - "sw a0, 8(sp)", - "sw a1, 12(sp)", - "sw a2, 16(sp)", - "sw a3, 20(sp)", - "sw a4, 24(sp)", - "sw a5, 28(sp)", - "sw a6, 32(sp)", - "sw a7, 36(sp)", - // Call Rust trap handler with pointer to the save area in a0. - "mv a0, sp", - "call {handler}", - // Restore sepc and registers, then return from trap. - "lw t0, 0(sp)", - "csrw sepc, t0", - "lw ra, 4(sp)", - "lw a0, 8(sp)", - "lw a1, 12(sp)", - "lw a2, 16(sp)", - "lw a3, 20(sp)", - "lw a4, 24(sp)", - "lw a5, 28(sp)", - "lw a6, 32(sp)", - "lw a7, 36(sp)", - "lw t0, 40(sp)", - "addi sp, sp, 44", - "csrrw sp, sscratch, sp", - "sret", - handler = sym handle_trap + core::arch::asm!( + "call {save} # save regs on kernel stack", + "call {handler} # run Rust trap handler", + "j {restore} # restore regs and return", + save = sym save_trap_frame, + handler = sym handle_trap, + restore = sym restore_trap_frame, + options(noreturn), ); - core::hint::unreachable_unchecked(); } } +#[unsafe(naked)] +unsafe extern "C" fn save_trap_frame() -> ! { + core::arch::naked_asm!( + // Switch to kernel stack and make room for saved registers. + "csrrw sp, sscratch, sp", + "addi sp, sp, -44", + // Save caller-saved registers we clobber and sepc. + "sw t0, 40(sp)", // t0 holds user satp from trap trampoline + "csrr t0, sepc", + "sw t0, 0(sp)", // saved sepc + "sw ra, 4(sp)", + "sw a0, 8(sp)", + "sw a1, 12(sp)", + "sw a2, 16(sp)", + "sw a3, 20(sp)", + "sw a4, 24(sp)", + "sw a5, 28(sp)", + "sw a6, 32(sp)", + "sw a7, 36(sp)", + "mv a0, sp", // return saved-area pointer in a0 + "ret", + ); +} + +#[unsafe(naked)] +unsafe extern "C" fn restore_trap_frame() -> ! { + core::arch::naked_asm!( + // Restore sepc and registers. + "lw t1, 0(sp)", + "csrw sepc, t1", + "lw ra, 4(sp)", + "lw a0, 8(sp)", + "lw a1, 12(sp)", + "lw a2, 16(sp)", + "lw a3, 20(sp)", + "lw a4, 24(sp)", + "lw a5, 28(sp)", + "lw a6, 32(sp)", + "lw a7, 36(sp)", + "lw t0, 40(sp)", // user satp from trap trampoline + "addi sp, sp, 44", + "csrrw sp, sscratch, sp", + "j {return}", + return = sym return_from_trap, + ); +} + +#[unsafe(naked)] +unsafe extern "C" fn return_from_trap() -> ! { + core::arch::naked_asm!( + "csrr t1, sstatus", + "andi t1, t1, {spp}", + "bnez t1, 1f", + "li t1, {tramp}", + "jr t1", + "1:", + "sret", + tramp = const TRAMPOLINE_VA, + spp = const SSTATUS_SPP, + ); +} + /// Rust-level trap handler. Receives a pointer to the saved register block /// laid out as: /// [0] sepc, [1] ra, [2] a0, [3] a1, [4] a2, [5] a3, [6] a4, [7] a5, -/// [8] a6, [9] a7, [10] t0. +/// [8] a6, [9] a7, [10] t0 (user satp from trap trampoline). #[unsafe(no_mangle)] pub extern "C" fn handle_trap(saved: *mut u32) { let regs = unsafe { core::slice::from_raw_parts_mut(saved, SAVED_REG_COUNT) }; From ade24f3a6234f933110f68f33cddd990240dc8e5 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 28 Dec 2025 22:57:32 +0200 Subject: [PATCH 39/70] Refactor kernel memory modules and wire kernel heap allocator Move kernel memory code under memory/ and rename modules - Move mmu to memory/page_allocator and update imports - Add memory/heap bump allocator and global allocator wiring - Initialize page allocator and heap from boot info in init - Add kernel panic handler with formatted message logging Adjust program features - Rename guest_handlers feature to guest and remove default - Enable guest feature for examples --- crates/examples/Cargo.toml | 2 +- crates/kernel/src/global.rs | 5 +- crates/kernel/src/init.rs | 15 ++-- crates/kernel/src/lib.rs | 25 +++++- crates/kernel/src/memory/heap.rs | 79 +++++++++++++++++++ crates/kernel/src/memory/mod.rs | 2 + .../src/{mmu.rs => memory/page_allocator.rs} | 0 crates/kernel/src/syscall/panic.rs | 2 +- crates/kernel/src/task/prep.rs | 3 +- crates/kernel/src/task/run.rs | 2 +- crates/program/Cargo.toml | 3 +- crates/program/src/lib.rs | 4 +- crates/program/src/panic.rs | 4 +- 13 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 crates/kernel/src/memory/heap.rs create mode 100644 crates/kernel/src/memory/mod.rs rename crates/kernel/src/{mmu.rs => memory/page_allocator.rs} (100%) diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index b992162..15c5539 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -program = { path = "../program" } +program = { path = "../program", features = ["guest"] } sha2 = { version = "0.10", default-features = false } k256 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdsa", "alloc"] } diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 3854b09..a07c9c9 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -4,7 +4,8 @@ use core::ptr; use state::State; use crate::Task; -use crate::mmu::PageAllocator; +use crate::memory::heap::BumpAllocator; +use crate::memory::page_allocator::PageAllocator; /// Minimal wrapper to store non-`Sync` types in statics. /// @@ -124,7 +125,7 @@ impl Drop for TaskList { #[allow(dead_code)] pub static TASKS: Global = Global::new(TaskList::new()); pub static STATE: Global> = Global::new(None); -pub static PAGE_ALLOC_INIT: Global = Global::new(false); pub static NEXT_ASID: Global = Global::new(1); pub static ROOT_PPN: Global = Global::new(0); pub static PAGE_ALLOC: Global> = Global::new(None); +pub static KERNEL_HEAP: Global = Global::new(BumpAllocator::empty()); diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index 9e0f230..108b3ce 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -3,13 +3,18 @@ use core::{cmp, slice}; use program::{log, logf}; use state::State; -use kernel::global::{CURRENT_TASK, KERNEL_TASK_SLOT, PAGE_ALLOC_INIT, STATE, TASKS}; -use kernel::{mmu, BootInfo, Task, trap}; +use kernel::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; +use kernel::{BootInfo, Task, trap}; +use kernel::memory::{heap, page_allocator}; /// Initialize kernel state from the bootloader handoff and optional state blob. pub fn init_kernel(state_ptr: *const u8, state_len: usize, boot_info_ptr: *const BootInfo) { let boot_info = unsafe { boot_info_ptr.as_ref() }; if let Some(info) = init_boot_info(boot_info) { + unsafe { + page_allocator::init(info); + heap::init(info.heap_ptr, info.va_base, info.va_len); + } trap::init_trap_vector(info.kstack_top); init_state(state_ptr, state_len); } else { @@ -44,12 +49,6 @@ fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { .unwrap_or(0) ); if let Some(info) = boot_info { - unsafe { - if !*PAGE_ALLOC_INIT.get_mut() { - mmu::init(info); - *PAGE_ALLOC_INIT.get_mut() = true; - } - } let task = Task::kernel( info.root_ppn, info.heap_ptr, diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 53be796..e1c9d01 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] #![feature(naked_functions)] +#![feature(alloc_error_handler)] pub mod config; pub use config::Config; @@ -8,6 +9,28 @@ pub mod global; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; pub use task::{prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; -pub mod mmu; +pub mod memory; pub mod trap; pub mod syscall; + +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let mut buf = [0u8; 256]; + let len = { + let mut writer = program::BufferWriter::new(&mut buf); + if write!(&mut writer, "{}", info).is_ok() { + writer.len() + } else { + 0 + } + }; + if len == 0 { + program::log!("kernel panic"); + } else { + program::logf!("kernel panic: %s", buf.as_ptr() as u32, len as u32); + } + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/memory/heap.rs b/crates/kernel/src/memory/heap.rs new file mode 100644 index 0000000..bb0dea6 --- /dev/null +++ b/crates/kernel/src/memory/heap.rs @@ -0,0 +1,79 @@ +use crate::global::Global; +use core::alloc::{GlobalAlloc, Layout}; +use core::ptr; + +#[derive(Clone, Copy)] +pub(crate) struct BumpAllocator { + next: usize, + end: usize, +} + +impl BumpAllocator { + pub(crate) const fn empty() -> Self { + Self { next: 0, end: 0 } + } + + fn init(&mut self, start: usize, end: usize) { + self.next = start; + self.end = end; + } + + fn alloc(&mut self, size: usize, align: usize) -> Option<*mut u8> { + if size == 0 || align == 0 || (align & (align - 1)) != 0 { + return None; + } + let start = align_up(self.next, align)?; + let end = start.checked_add(size)?; + if end > self.end { + return None; + } + self.next = end; + Some(start as *mut u8) + } +} + +/// Initialize the kernel bump allocator using the bootloader-provided heap pointer +/// and the mapped kernel VA window. +pub fn init(heap_ptr: u32, va_base: u32, va_len: u32) { + let start = heap_ptr as usize; + let end = (va_base as usize).saturating_add(va_len as usize); + unsafe { + crate::global::KERNEL_HEAP.get_mut().init(start, end); + } +} + +/// Allocate a kernel buffer from the bump allocator. +/// +/// Returns a kernel virtual address on success, or None on exhaustion/invalid args. +pub fn alloc(size: usize, align: usize) -> Option<*mut u8> { + unsafe { + crate::global::KERNEL_HEAP.get_mut().alloc(size, align) + } +} + +fn align_up(value: usize, align: usize) -> Option { + let mask = align - 1; + value.checked_add(mask).map(|v| v & !mask) +} + +struct KernelAlloc; + +unsafe impl GlobalAlloc for KernelAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + alloc(layout.size(), layout.align()).unwrap_or(ptr::null_mut()) + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {} +} + +#[global_allocator] +static KERNEL_ALLOC: KernelAlloc = KernelAlloc; + +#[alloc_error_handler] +fn alloc_error(layout: Layout) -> ! { + panic!( + "kernel alloc error: size={} align={}", + layout.size(), + layout.align() + ); +} diff --git a/crates/kernel/src/memory/mod.rs b/crates/kernel/src/memory/mod.rs new file mode 100644 index 0000000..dd79fae --- /dev/null +++ b/crates/kernel/src/memory/mod.rs @@ -0,0 +1,2 @@ +pub mod heap; +pub mod page_allocator; diff --git a/crates/kernel/src/mmu.rs b/crates/kernel/src/memory/page_allocator.rs similarity index 100% rename from crates/kernel/src/mmu.rs rename to crates/kernel/src/memory/page_allocator.rs diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs index 9ea10db..0e16591 100644 --- a/crates/kernel/src/syscall/panic.rs +++ b/crates/kernel/src/syscall/panic.rs @@ -2,7 +2,7 @@ use program::{log, logf}; use types::{SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; use crate::global::{CURRENT_TASK, TASKS}; -use crate::mmu; +use crate::memory::page_allocator as mmu; pub(crate) fn sys_panic_with_message(msg_ptr: u32, msg_len: u32) -> u32 { if msg_ptr == 0 || msg_len == 0 { diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 32110f1..7a41765 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,4 +1,5 @@ -use crate::{AddressSpace, Config, Task, mmu}; +use crate::{AddressSpace, Config, Task}; +use crate::memory::page_allocator as mmu; use program::{log, logf}; use types::address::Address; diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index 31e2ae9..5d32991 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -1,5 +1,5 @@ use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; -use crate::mmu; +use crate::memory::page_allocator as mmu; use program::logf; use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_RA, REG_SP, TRAMPOLINE_VA, TRAP_TRAMPOLINE_VA}; diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml index 7c45774..43ba48c 100644 --- a/crates/program/Cargo.toml +++ b/crates/program/Cargo.toml @@ -4,8 +4,7 @@ version = "0.1.0" edition = "2024" [features] -default = ["guest_handlers"] -guest_handlers = [] +guest = [] [dependencies] types = { path = "../types" } diff --git a/crates/program/src/lib.rs b/crates/program/src/lib.rs index 875e1d1..b04cfe7 100644 --- a/crates/program/src/lib.rs +++ b/crates/program/src/lib.rs @@ -54,14 +54,14 @@ pub use storage::PERSISTENT_DOMAIN; // Allow `$crate::PERSISTENT_DOMAIN` in macr pub mod router; pub use router::{decode_calls, route, FuncCall}; -// Panic helper (and panic handler when guest_handlers enabled) +// Panic helper (and panic handler when guest feature enabled) pub mod panic; pub use panic::vm_panic; // Memory allocator pub mod allocator; -#[cfg(all(target_arch = "riscv32", feature = "guest_handlers"))] +#[cfg(all(target_arch = "riscv32", feature = "guest"))] #[global_allocator] static ALLOCATOR: allocator::VmAllocator = allocator::VmAllocator; diff --git a/crates/program/src/panic.rs b/crates/program/src/panic.rs index 663703c..01c1974 100644 --- a/crates/program/src/panic.rs +++ b/crates/program/src/panic.rs @@ -23,8 +23,8 @@ pub fn vm_panic(msg: &[u8]) -> ! { } } -/// Guest panic handler for RISC-V builds (only when guest_handlers enabled). -#[cfg(all(target_arch = "riscv32", feature = "guest_handlers"))] +/// Guest panic handler for RISC-V builds (only when guest feature enabled). +#[cfg(all(target_arch = "riscv32", feature = "guest"))] #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { use core::fmt::Write; From 309aa75371c03520005960c808c3ac3cf5bbabe3 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 28 Dec 2025 23:03:18 +0200 Subject: [PATCH 40/70] restrict syscall alloc from kernel (using its own allocator) --- crates/kernel/src/syscall/alloc.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs index 1f5b626..67a0edd 100644 --- a/crates/kernel/src/syscall/alloc.rs +++ b/crates/kernel/src/syscall/alloc.rs @@ -1,6 +1,6 @@ use program::{log, logf}; -use crate::global::{CURRENT_TASK, TASKS}; +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { let size = args[0]; let align = args[1]; @@ -15,6 +15,11 @@ pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { } let current = unsafe { *CURRENT_TASK.get_mut() }; + // Kernel task should never call sys_alloc. + if current == KERNEL_TASK_SLOT { + panic!("sys_alloc: kernel task cannot allocate memory"); + } + let tasks = unsafe { TASKS.get_mut() }; let task = match tasks.get_mut(current) { Some(task) => task, @@ -57,6 +62,11 @@ pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { } pub(crate) fn sys_dealloc(_args: [u32; 6]) -> u32 { + let current = unsafe { *CURRENT_TASK.get_mut() }; + // Kernel task should never call sys_alloc. + if current == KERNEL_TASK_SLOT { + panic!("sys_alloc: kernel task cannot allocate memory"); + } // No-op: kernel heap is bump-only for now. 0 } From 53d4aff9082af6b06dcf509d419736b0febe6096 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 29 Dec 2025 08:12:55 +0200 Subject: [PATCH 41/70] sys storage --- crates/kernel/src/syscall/mod.rs | 12 +- crates/kernel/src/syscall/storage.rs | 211 +++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 crates/kernel/src/syscall/storage.rs diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index bbb95f6..6402a76 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -5,9 +5,11 @@ use program::{log, logf}; pub mod alloc; pub mod panic; +pub mod storage; use alloc::{sys_alloc, sys_dealloc}; use panic::sys_panic; +use storage::{sys_storage_get, sys_storage_set}; pub(crate) use panic::sys_panic_with_message; pub const SYSCALL_STORAGE_GET: u32 = 1; @@ -48,16 +50,6 @@ pub fn dispatch_syscall(call_id: u32, args: [u32; 6], caller_mode: CallerMode) - } } -fn sys_storage_get(_args: [u32; 6]) -> u32 { - log!("sys_storage_get: need implementation"); - 0 -} - -fn sys_storage_set(_args: [u32; 6]) -> u32 { - log!("sys_storage_set: need implementation"); - 0 -} - fn sys_log(_args: [u32; 6], caller_mode: CallerMode) -> u32 { if caller_mode == CallerMode::Supervisor { log!("kernel: sys_log: need implementation"); diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs new file mode 100644 index 0000000..5db4564 --- /dev/null +++ b/crates/kernel/src/syscall/storage.rs @@ -0,0 +1,211 @@ +extern crate alloc; + +use alloc::{format, string::String, vec, vec::Vec}; +use core::cmp; + +use program::{log, logf}; +use types::{Address, ADDRESS_LEN, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; + +use crate::global::{CURRENT_TASK, STATE, TASKS}; +use crate::memory::page_allocator as mmu; +use crate::syscall::alloc::sys_alloc; +use state::State; + +pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { + let address_ptr = args[0]; + let domain_ptr = args[1]; + let key_ptr = args[2]; + let lens_packed = args[3] as usize; + let domain_len = lens_packed & 0xffff; + let key_len = lens_packed >> 16; + + let root_ppn = match current_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let address_bytes = match read_user_bytes(root_ppn, address_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + let mut addr_buf = [0u8; ADDRESS_LEN]; + if address_bytes.len() != ADDRESS_LEN { + log!("sys_storage_get: invalid address length"); + return 0; + } + addr_buf.copy_from_slice(&address_bytes); + let address = Address(addr_buf); + + let domain_bytes = match read_user_bytes(root_ppn, domain_ptr, domain_len) { + Some(bytes) => bytes, + None => return 0, + }; + let domain = match core::str::from_utf8(&domain_bytes) { + Ok(s) => s, + Err(_) => { + log!("sys_storage_get: invalid domain utf8"); + return 0; + } + }; + + let key_bytes = match read_user_bytes(root_ppn, key_ptr, key_len) { + Some(bytes) => bytes, + None => return 0, + }; + let key_hex = hex_encode(&key_bytes); + let composite_key = format!("{}:{}", domain, key_hex); + + let value = unsafe { STATE.get_mut() } + .as_ref() + .and_then(|state| state.get_account(&address)) + .and_then(|account| account.storage.get(&composite_key).cloned()); + + let value = match value { + Some(value) => value, + None => return 0, + }; + + let total_len = match value.len().checked_add(4) { + Some(len) => len, + None => { + log!("sys_storage_get: value too large"); + return 0; + } + }; + if total_len > u32::MAX as usize { + log!("sys_storage_get: value exceeds u32 size"); + return 0; + } + + let addr = sys_alloc([total_len as u32, 8, 0, 0, 0, 0]); + if addr == 0 { + log!("sys_storage_get: allocation failed"); + return 0; + } + + let mut buf = Vec::with_capacity(total_len); + buf.extend_from_slice(&(value.len() as u32).to_le_bytes()); + buf.extend_from_slice(&value); + + if !mmu::copy_into_user(root_ppn, addr, &buf) { + logf!("sys_storage_get: failed to write to 0x%x", addr); + return 0; + } + + addr +} + +pub(crate) fn sys_storage_set(args: [u32; 6]) -> u32 { + let address_ptr = args[0]; + let domain_ptr = args[1]; + let key_ptr = args[2]; + let lens_packed = args[3] as usize; + let val_ptr = args[4]; + let val_len = args[5] as usize; + + let domain_len = lens_packed & 0xffff; + let key_len = lens_packed >> 16; + + let root_ppn = match current_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let address_bytes = match read_user_bytes(root_ppn, address_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + if address_bytes.len() != ADDRESS_LEN { + log!("sys_storage_set: invalid address length"); + return 0; + } + let mut addr_buf = [0u8; ADDRESS_LEN]; + addr_buf.copy_from_slice(&address_bytes); + let address = Address(addr_buf); + + let domain_bytes = match read_user_bytes(root_ppn, domain_ptr, domain_len) { + Some(bytes) => bytes, + None => return 0, + }; + let domain = match core::str::from_utf8(&domain_bytes) { + Ok(s) => s, + Err(_) => { + log!("sys_storage_set: invalid domain utf8"); + return 0; + } + }; + + let key_bytes = match read_user_bytes(root_ppn, key_ptr, key_len) { + Some(bytes) => bytes, + None => return 0, + }; + let key_hex = hex_encode(&key_bytes); + + let value = match read_user_bytes(root_ppn, val_ptr, val_len) { + Some(bytes) => bytes, + None => return 0, + }; + + let composite_key = format!("{}:{}", domain, key_hex); + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + state + .get_account_mut(&address) + .storage + .insert(composite_key, value); + 0 +} + +fn current_root_ppn() -> Option { + let current = unsafe { *CURRENT_TASK.get_mut() }; + let tasks = unsafe { TASKS.get_mut() }; + match tasks.get(current) { + Some(task) => Some(task.addr_space.root_ppn), + None => { + logf!("sys_storage: no current task for slot %d", current as u32); + None + } + } +} + +fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option> { + if len == 0 { + return Some(Vec::new()); + } + let mut buf = vec![0u8; len]; + let mut remaining = len; + let mut dst_off = 0usize; + let mut va = ptr; + while remaining > 0 { + let phys = match mmu::translate_user_va(root_ppn, va) { + Some(p) => p, + None => { + logf!("sys_storage: invalid memory access 0x%x", va); + return None; + } + }; + let page_off = (va as usize) & (SV32_PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, SV32_PAGE_SIZE - page_off); + let src = SV32_DIRECT_MAP_BASE as usize + phys; + unsafe { + core::ptr::copy_nonoverlapping( + src as *const u8, + buf.as_mut_ptr().add(dst_off), + to_copy, + ); + } + remaining -= to_copy; + dst_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + Some(buf) +} + +fn hex_encode(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = Vec::with_capacity(bytes.len().saturating_mul(2)); + for &b in bytes { + out.push(HEX[(b >> 4) as usize]); + out.push(HEX[(b & 0x0f) as usize]); + } + String::from_utf8(out).unwrap_or_default() +} From ef235dc08296cabe32f01fe449f89a5055d01b98 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 29 Dec 2025 17:38:55 +0200 Subject: [PATCH 42/70] Refactor trap handling into module Move trap logic into a new trap module directory and split save/restore assembly into dedicated files for readability. Keep the trap entry flow intact while wiring module-level helpers. This change removes the old trap.rs file and introduces trap/mod.rs plus save/restore trap frame sources. --- crates/kernel/src/bundle.rs | 2 +- crates/kernel/src/{trap.rs => trap/mod.rs} | 93 ++++++++------------ crates/kernel/src/trap/restore_trap_frame.rs | 47 ++++++++++ crates/kernel/src/trap/save_trap_frame.rs | 43 +++++++++ 4 files changed, 127 insertions(+), 58 deletions(-) rename crates/kernel/src/{trap.rs => trap/mod.rs} (63%) create mode 100644 crates/kernel/src/trap/restore_trap_frame.rs create mode 100644 crates/kernel/src/trap/save_trap_frame.rs diff --git a/crates/kernel/src/bundle.rs b/crates/kernel/src/bundle.rs index d63e07c..803c24e 100644 --- a/crates/kernel/src/bundle.rs +++ b/crates/kernel/src/bundle.rs @@ -30,6 +30,6 @@ fn execute_transaction(tx: &Transaction) { match tx.tx_type { TransactionType::CreateAccount => create_account(tx), TransactionType::ProgramCall => program_call(tx), - _ => log!("executing transaction"), + _ => panic!("unsupported transaction type"), } } diff --git a/crates/kernel/src/trap.rs b/crates/kernel/src/trap/mod.rs similarity index 63% rename from crates/kernel/src/trap.rs rename to crates/kernel/src/trap/mod.rs index 8571574..b313d9f 100644 --- a/crates/kernel/src/trap.rs +++ b/crates/kernel/src/trap/mod.rs @@ -4,10 +4,27 @@ use program::{log, logf}; use crate::syscall; use crate::task::TRAMPOLINE_VA; +mod save_trap_frame; +mod restore_trap_frame; + +use restore_trap_frame::restore_trap_frame; +use save_trap_frame::save_trap_frame; + const SCAUSE_ECALL_FROM_U: usize = 8; const SCAUSE_ECALL_FROM_S: usize = 9; const SSTATUS_SPP: u32 = 1 << 8; -const SAVED_REG_COUNT: usize = 11; +const REG_COUNT: usize = 32; +const TRAP_FRAME_WORDS: usize = REG_COUNT + 1; // regs + pc +const TRAP_FRAME_BYTES: i32 = (TRAP_FRAME_WORDS * 4) as i32; +const REG_A0: usize = 10; +const REG_A1: usize = 11; +const REG_A2: usize = 12; +const REG_A3: usize = 13; +const REG_A4: usize = 14; +const REG_A5: usize = 15; +const REG_A6: usize = 16; +const REG_A7: usize = 17; +const REG_PC: usize = 32; /// Install the kernel trap vector and set up the kernel stack for traps. pub fn init_trap_vector(kstack_top: u32) { @@ -29,9 +46,11 @@ pub fn init_trap_vector(kstack_top: u32) { pub unsafe extern "C" fn trap_entry() -> ! { unsafe { core::arch::asm!( + "call {swap} # switch to kernel stack and reserve trap frame", "call {save} # save regs on kernel stack", "call {handler} # run Rust trap handler", "j {restore} # restore regs and return", + swap = sym swap_to_kernel_stack, save = sym save_trap_frame, handler = sym handle_trap, restore = sym restore_trap_frame, @@ -41,49 +60,12 @@ pub unsafe extern "C" fn trap_entry() -> ! { } #[unsafe(naked)] -unsafe extern "C" fn save_trap_frame() -> ! { +unsafe extern "C" fn swap_to_kernel_stack() -> ! { core::arch::naked_asm!( - // Switch to kernel stack and make room for saved registers. "csrrw sp, sscratch, sp", - "addi sp, sp, -44", - // Save caller-saved registers we clobber and sepc. - "sw t0, 40(sp)", // t0 holds user satp from trap trampoline - "csrr t0, sepc", - "sw t0, 0(sp)", // saved sepc - "sw ra, 4(sp)", - "sw a0, 8(sp)", - "sw a1, 12(sp)", - "sw a2, 16(sp)", - "sw a3, 20(sp)", - "sw a4, 24(sp)", - "sw a5, 28(sp)", - "sw a6, 32(sp)", - "sw a7, 36(sp)", - "mv a0, sp", // return saved-area pointer in a0 + "addi sp, sp, -{frame_bytes}", "ret", - ); -} - -#[unsafe(naked)] -unsafe extern "C" fn restore_trap_frame() -> ! { - core::arch::naked_asm!( - // Restore sepc and registers. - "lw t1, 0(sp)", - "csrw sepc, t1", - "lw ra, 4(sp)", - "lw a0, 8(sp)", - "lw a1, 12(sp)", - "lw a2, 16(sp)", - "lw a3, 20(sp)", - "lw a4, 24(sp)", - "lw a5, 28(sp)", - "lw a6, 32(sp)", - "lw a7, 36(sp)", - "lw t0, 40(sp)", // user satp from trap trampoline - "addi sp, sp, 44", - "csrrw sp, sscratch, sp", - "j {return}", - return = sym return_from_trap, + frame_bytes = const TRAP_FRAME_BYTES, ); } @@ -104,17 +86,14 @@ unsafe extern "C" fn return_from_trap() -> ! { /// Rust-level trap handler. Receives a pointer to the saved register block /// laid out as: -/// [0] sepc, [1] ra, [2] a0, [3] a1, [4] a2, [5] a3, [6] a4, [7] a5, -/// [8] a6, [9] a7, [10] t0 (user satp from trap trampoline). +/// regs[0..32] = x0..x31, regs[32] = pc. #[unsafe(no_mangle)] pub extern "C" fn handle_trap(saved: *mut u32) { - let regs = unsafe { core::slice::from_raw_parts_mut(saved, SAVED_REG_COUNT) }; + let regs = unsafe { core::slice::from_raw_parts_mut(saved, TRAP_FRAME_WORDS) }; let scause = read_scause(); let stval = read_stval(); - let sepc = regs[0]; - let sp: u32; - unsafe { asm!("mv {0}, sp", out(reg) sp); } - + let sepc = regs[REG_PC]; + let is_interrupt = (scause >> 31) != 0; if is_interrupt { panic!( @@ -127,22 +106,22 @@ pub extern "C" fn handle_trap(saved: *mut u32) { match code { SCAUSE_ECALL_FROM_U | SCAUSE_ECALL_FROM_S => { let args = [ - regs[3], // a1 - regs[4], // a2 - regs[5], // a3 - regs[6], // a4 - regs[7], // a5 - regs[8], // a6 + regs[REG_A1], + regs[REG_A2], + regs[REG_A3], + regs[REG_A4], + regs[REG_A5], + regs[REG_A6], ]; - let call_id = regs[9]; // a7 + let call_id = regs[REG_A7]; let caller_mode = if read_sstatus() & SSTATUS_SPP != 0 { syscall::CallerMode::Supervisor } else { syscall::CallerMode::User }; let ret = syscall::dispatch_syscall(call_id, args, caller_mode); - regs[2] = ret; // a0 return value - regs[0] = regs[0].wrapping_add(4); // Advance past ecall + regs[REG_A0] = ret; // a0 return value + regs[REG_PC] = regs[REG_PC].wrapping_add(4); // Advance past ecall } _ => log!("unhandled trap"), } diff --git a/crates/kernel/src/trap/restore_trap_frame.rs b/crates/kernel/src/trap/restore_trap_frame.rs new file mode 100644 index 0000000..9f497a2 --- /dev/null +++ b/crates/kernel/src/trap/restore_trap_frame.rs @@ -0,0 +1,47 @@ +use super::{return_from_trap, TRAP_FRAME_BYTES}; + +#[unsafe(naked)] +pub(super) unsafe extern "C" fn restore_trap_frame() -> ! { + core::arch::naked_asm!( + // Restore sepc and registers. + "lw t1, 128(sp)", + "csrw sepc, t1", + "lw t2, 8(sp)", + "csrw sscratch, t2", // restore user sp for swap + "lw ra, 4(sp)", + "lw gp, 12(sp)", + "lw tp, 16(sp)", + "lw t0, 20(sp)", // user satp from trap trampoline + "lw s0, 32(sp)", + "lw s1, 36(sp)", + "lw a0, 40(sp)", + "lw a1, 44(sp)", + "lw a2, 48(sp)", + "lw a3, 52(sp)", + "lw a4, 56(sp)", + "lw a5, 60(sp)", + "lw a6, 64(sp)", + "lw a7, 68(sp)", + "lw s2, 72(sp)", + "lw s3, 76(sp)", + "lw s4, 80(sp)", + "lw s5, 84(sp)", + "lw s6, 88(sp)", + "lw s7, 92(sp)", + "lw s8, 96(sp)", + "lw s9, 100(sp)", + "lw s10, 104(sp)", + "lw s11, 108(sp)", + "lw t3, 112(sp)", + "lw t4, 116(sp)", + "lw t5, 120(sp)", + "lw t6, 124(sp)", + "lw t1, 24(sp)", + "lw t2, 28(sp)", + "addi sp, sp, {frame_bytes}", + "csrrw sp, sscratch, sp", + "j {return}", + return = sym return_from_trap, + frame_bytes = const TRAP_FRAME_BYTES, + ); +} diff --git a/crates/kernel/src/trap/save_trap_frame.rs b/crates/kernel/src/trap/save_trap_frame.rs new file mode 100644 index 0000000..80e07a6 --- /dev/null +++ b/crates/kernel/src/trap/save_trap_frame.rs @@ -0,0 +1,43 @@ +#[unsafe(naked)] +pub(super) unsafe extern "C" fn save_trap_frame() -> ! { + core::arch::naked_asm!( + // Save all GPRs + sepc (PC). User sp lives in sscratch after the swap. + "sw zero, 0(sp)", // x0 + "sw ra, 4(sp)", // x1 + "sw t1, 24(sp)", // x6 (save before clobber) + "csrr t1, sscratch", // user sp + "sw t1, 8(sp)", // x2 + "sw gp, 12(sp)", // x3 + "sw tp, 16(sp)", // x4 + "sw t0, 20(sp)", // x5 (user satp from trap trampoline) + "sw t2, 28(sp)", // x7 + "sw s0, 32(sp)", // x8 + "sw s1, 36(sp)", // x9 + "sw a0, 40(sp)", // x10 + "sw a1, 44(sp)", // x11 + "sw a2, 48(sp)", // x12 + "sw a3, 52(sp)", // x13 + "sw a4, 56(sp)", // x14 + "sw a5, 60(sp)", // x15 + "sw a6, 64(sp)", // x16 + "sw a7, 68(sp)", // x17 + "sw s2, 72(sp)", // x18 + "sw s3, 76(sp)", // x19 + "sw s4, 80(sp)", // x20 + "sw s5, 84(sp)", // x21 + "sw s6, 88(sp)", // x22 + "sw s7, 92(sp)", // x23 + "sw s8, 96(sp)", // x24 + "sw s9, 100(sp)", // x25 + "sw s10, 104(sp)", // x26 + "sw s11, 108(sp)", // x27 + "sw t3, 112(sp)", // x28 + "sw t4, 116(sp)", // x29 + "sw t5, 120(sp)", // x30 + "sw t6, 124(sp)", // x31 + "csrr t1, sepc", + "sw t1, 128(sp)", // pc + "mv a0, sp", // return saved-area pointer in a0 + "ret", + ); +} From 7d30b04b268a4d499c477749ce1b9da145379334 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 29 Dec 2025 17:40:52 +0200 Subject: [PATCH 43/70] moved files --- crates/kernel/src/{ => bundle}/create_account.rs | 0 crates/kernel/src/{bundle.rs => bundle/mod.rs} | 7 +++++-- crates/kernel/src/{ => bundle}/program_call.rs | 0 crates/kernel/src/main.rs | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) rename crates/kernel/src/{ => bundle}/create_account.rs (100%) rename crates/kernel/src/{bundle.rs => bundle/mod.rs} (90%) rename crates/kernel/src/{ => bundle}/program_call.rs (100%) diff --git a/crates/kernel/src/create_account.rs b/crates/kernel/src/bundle/create_account.rs similarity index 100% rename from crates/kernel/src/create_account.rs rename to crates/kernel/src/bundle/create_account.rs diff --git a/crates/kernel/src/bundle.rs b/crates/kernel/src/bundle/mod.rs similarity index 90% rename from crates/kernel/src/bundle.rs rename to crates/kernel/src/bundle/mod.rs index 803c24e..5a5d216 100644 --- a/crates/kernel/src/bundle.rs +++ b/crates/kernel/src/bundle/mod.rs @@ -3,8 +3,11 @@ use core::mem::forget; use program::{log, logf}; use types::transaction::{Transaction, TransactionBundle, TransactionType}; -use crate::create_account::create_account; -use crate::program_call::program_call; +mod create_account; +mod program_call; + +use self::create_account::create_account; +use self::program_call::program_call; pub(crate) fn process_bundle(encoded_bundle: &[u8]) { log!("processing transaction bundle"); diff --git a/crates/kernel/src/program_call.rs b/crates/kernel/src/bundle/program_call.rs similarity index 100% rename from crates/kernel/src/program_call.rs rename to crates/kernel/src/bundle/program_call.rs diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 7946227..484d0ed 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -9,8 +9,6 @@ use program::{log, logf}; mod init; mod bundle; -mod create_account; -mod program_call; use crate::bundle::process_bundle; use crate::init::init_kernel; From 7ddeacdde9e0ef9ddd1d76150c0b19163f47a796 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Mon, 29 Dec 2025 22:10:21 +0200 Subject: [PATCH 44/70] kernel: add kernel_run_task wrapper - save full kernel register state into TASKS[0] before handoff - keep run_task focused on user-mode entry only - export new wrapper and update program_call to use it - add inline asm comments for register snapshot and trampoline jump --- crates/kernel/src/bundle/program_call.rs | 4 +- crates/kernel/src/lib.rs | 2 +- crates/kernel/src/task/mod.rs | 7 +- crates/kernel/src/task/run.rs | 104 ++++++++++++++++------- 4 files changed, 82 insertions(+), 35 deletions(-) diff --git a/crates/kernel/src/bundle/program_call.rs b/crates/kernel/src/bundle/program_call.rs index dfefa53..7a8f12a 100644 --- a/crates/kernel/src/bundle/program_call.rs +++ b/crates/kernel/src/bundle/program_call.rs @@ -1,6 +1,6 @@ use alloc::format; -use kernel::{prep_program_task, run_task, Config, PROGRAM_WINDOW_BYTES}; +use kernel::{kernel_run_task, prep_program_task, Config, PROGRAM_WINDOW_BYTES}; use kernel::global::{STATE, TASKS}; use program::{log, logf}; use program::parser::HexCodec; @@ -89,7 +89,7 @@ pub(crate) fn program_call(tx: &Transaction) { return; } let current = tasks_slot.len().saturating_sub(1); - run_task(current); + kernel_run_task(current); } } else { log!("Program call skipped: no memory manager installed"); diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index e1c9d01..fffe9ce 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -8,7 +8,7 @@ pub use types::boot::BootInfo; pub mod global; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; -pub use task::{prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; +pub use task::{kernel_run_task, prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; pub mod memory; pub mod trap; pub mod syscall; diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index ca9948e..5ba3e92 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -31,8 +31,11 @@ // a0..a3 = to/from/input_base/input_len // Caller can push the task into TASKS for bookkeeping. // +// kernel_run_task(task): +// - Save the current kernel register file (x0-x31 + pc) into TASKS[0]. +// - Run the task (same behavior as run_task). +// // run_task(task): -// - Save the current kernel frame (sp/ra/pc) into TASKS[0] for a future return path. // - Preload t0 with the task root (satp value); load user sp and a0..a3; clear ra. // - Set sepc to the user PC and clear sstatus.SPP so sret enters user mode. // - Set stvec to the trap trampoline VA. @@ -55,7 +58,7 @@ pub mod run; pub use task::{AddressSpace, Task, TrapFrame}; pub use prep::prep_program_task; -pub use run::run_task; +pub use run::{kernel_run_task, run_task}; const PAGE_SIZE: usize = 4096; const STACK_BYTES: usize = 0x4000; // 16 KiB user stack diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index 5d32991..f01dc9e 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -2,12 +2,15 @@ use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; use crate::memory::page_allocator as mmu; use program::logf; -use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_RA, REG_SP, TRAMPOLINE_VA, TRAP_TRAMPOLINE_VA}; +use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, TRAMPOLINE_VA, TRAP_TRAMPOLINE_VA}; const SSTATUS_SPP: u32 = 1 << 8; +const REG_COUNT: usize = 32; +const REG_PC: usize = 32; +const TRAP_FRAME_WORDS: usize = REG_COUNT + 1; // regs + pc +const TRAP_FRAME_BYTES: i32 = (TRAP_FRAME_WORDS * 4) as i32; /// One-way context switch into a user task: -/// - Saves the current kernel frame into TASKS[0] /// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) pub fn run_task(task_idx: usize) { let (target_root, asid, pc, sp, a0, a1, a2, a3) = unsafe { @@ -42,31 +45,6 @@ pub fn run_task(task_idx: usize) { pc, sp, ); - // Save the current kernel frame (SP/RA/PC) into the kernel task slot (index 0). - let mut saved_sp: u32; - let mut saved_ra: u32; - let mut saved_pc: u32; - unsafe { - core::arch::asm!("mv {out}, sp", out = out(reg) saved_sp); - core::arch::asm!("mv {out}, ra", out = out(reg) saved_ra); - core::arch::asm!("auipc {out}, 0", out = out(reg) saved_pc); - } - logf!( - "run_task: saved kernel frame sp=0x%x ra=0x%x pc=0x%x", - saved_sp, - saved_ra, - saved_pc - ); - // Stash the kernel context so a future return path could restore it. - unsafe { - let tasks = TASKS.get_mut(); - if let Some(kernel_task) = tasks.get_mut(KERNEL_TASK_SLOT) { - kernel_task.addr_space.root_ppn = kernel_root; - kernel_task.tf.regs[REG_SP] = saved_sp; - kernel_task.tf.regs[REG_RA] = saved_ra; - kernel_task.tf.pc = saved_pc; - } - } // Prepare to enter user mode via sret: set sepc and clear sstatus.SPP. let mut sstatus: u32; unsafe { @@ -85,9 +63,9 @@ pub fn run_task(task_idx: usize) { // control to the user PC. unsafe { core::arch::asm!( - "mv ra, zero", - "mv sp, t2", - "jr t3", + "mv ra, zero # clear return address for one-way jump", + "mv sp, t2 # load user stack pointer", + "jr t3 # jump to shared trampoline", in("t0") target_root, in("a0") a0, in("a1") a1, @@ -99,3 +77,69 @@ pub fn run_task(task_idx: usize) { ); } } + +/// Save the full kernel register set into TASKS[0] and then run the task. +#[unsafe(naked)] +pub unsafe extern "C" fn kernel_run_task(task_idx: usize) -> ! { + core::arch::naked_asm!( + "addi sp, sp, -{frame_bytes} # reserve space for regs + pc", + "sw zero, 0(sp) # save x0", + "sw ra, 4(sp) # save x1", + "sw t1, 24(sp) # save x6 before clobber", + "addi t1, sp, {frame_bytes} # compute original sp", + "sw t1, 8(sp) # save x2 (original sp)", + "sw gp, 12(sp) # save x3", + "sw tp, 16(sp) # save x4", + "sw t0, 20(sp) # save x5", + "sw t2, 28(sp) # save x7", + "sw s0, 32(sp) # save x8", + "sw s1, 36(sp) # save x9", + "sw a0, 40(sp) # save x10", + "sw a1, 44(sp) # save x11", + "sw a2, 48(sp) # save x12", + "sw a3, 52(sp) # save x13", + "sw a4, 56(sp) # save x14", + "sw a5, 60(sp) # save x15", + "sw a6, 64(sp) # save x16", + "sw a7, 68(sp) # save x17", + "sw s2, 72(sp) # save x18", + "sw s3, 76(sp) # save x19", + "sw s4, 80(sp) # save x20", + "sw s5, 84(sp) # save x21", + "sw s6, 88(sp) # save x22", + "sw s7, 92(sp) # save x23", + "sw s8, 96(sp) # save x24", + "sw s9, 100(sp) # save x25", + "sw s10, 104(sp) # save x26", + "sw s11, 108(sp) # save x27", + "sw t3, 112(sp) # save x28", + "sw t4, 116(sp) # save x29", + "sw t5, 120(sp) # save x30", + "sw t6, 124(sp) # save x31", + "auipc t1, 0 # read current pc", + "sw t1, 128(sp) # save pc", + "mv a1, a0 # move task_idx into a1", + "mv a0, sp # pass saved regs pointer in a0", + "call {helper} # save into kernel task and run", + frame_bytes = const TRAP_FRAME_BYTES, + helper = sym kernel_run_task_inner, + ); +} + +extern "C" fn kernel_run_task_inner(saved: *const u32, task_idx: usize) -> ! { + // Interpret the saved trap-frame as regs[0..31] + pc and copy it into TASKS[0]. + let regs = unsafe { core::slice::from_raw_parts(saved, TRAP_FRAME_WORDS) }; + let kernel_root = mmu::current_root(); + unsafe { + let tasks = TASKS.get_mut(); + if let Some(kernel_task) = tasks.get_mut(KERNEL_TASK_SLOT) { + kernel_task.addr_space.root_ppn = kernel_root; + for (idx, value) in regs.iter().take(REG_COUNT).enumerate() { + kernel_task.tf.regs[idx] = *value; + } + kernel_task.tf.pc = regs[REG_PC]; + } + } + run_task(task_idx); + unsafe { core::hint::unreachable_unchecked() } +} From d217cf57794b98bb23b11420635649978f477136 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 30 Dec 2025 12:41:52 +0200 Subject: [PATCH 45/70] WIP trap from program --- crates/kernel/src/bundle/program_call.rs | 8 +- crates/kernel/src/task/prep.rs | 3 + crates/kernel/src/task/run.rs | 15 ++++ crates/kernel/src/task/task.rs | 3 + crates/kernel/src/trap/mod.rs | 74 +++++++++++++++++- crates/kernel/src/trap/restore_trap_frame.rs | 81 +++++++++++--------- crates/vm/src/cpu.rs | 1 + crates/vm/src/exe.rs | 8 +- 8 files changed, 152 insertions(+), 41 deletions(-) diff --git a/crates/kernel/src/bundle/program_call.rs b/crates/kernel/src/bundle/program_call.rs index 7a8f12a..c901cff 100644 --- a/crates/kernel/src/bundle/program_call.rs +++ b/crates/kernel/src/bundle/program_call.rs @@ -89,7 +89,13 @@ pub(crate) fn program_call(tx: &Transaction) { return; } let current = tasks_slot.len().saturating_sub(1); - kernel_run_task(current); + core::arch::asm!( + "la ra, 1f", + "j {run}", + "1:", + run = sym kernel_run_task, + in("a0") current, + ); } } else { log!("Program call skipped: no memory manager installed"); diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 7a41765..ad37a1d 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,4 +1,5 @@ use crate::{AddressSpace, Config, Task}; +use crate::global::CURRENT_TASK; use crate::memory::page_allocator as mmu; use program::{log, logf}; use types::address::Address; @@ -187,6 +188,8 @@ pub fn prep_program_task( ), Config::HEAP_START_ADDR as u32, ); + let caller = unsafe { *CURRENT_TASK.get_mut() }; + task.caller_task_id = Some(caller); // Set up initial trapframe. let stack_top = PROGRAM_VA_BASE .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index f01dc9e..6bfb87e 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -9,6 +9,7 @@ const REG_COUNT: usize = 32; const REG_PC: usize = 32; const TRAP_FRAME_WORDS: usize = REG_COUNT + 1; // regs + pc const TRAP_FRAME_BYTES: i32 = (TRAP_FRAME_WORDS * 4) as i32; +const REG_RA: usize = 1; /// One-way context switch into a user task: /// - Loads the task's satp/regs/pc and jumps to user code (no return path yet) @@ -45,6 +46,20 @@ pub fn run_task(task_idx: usize) { pc, sp, ); + unsafe { + if let Some(task) = TASKS.get_mut().get(task_idx) { + if let Some(caller_idx) = task.caller_task_id { + if let Some(caller_task) = TASKS.get_mut().get(caller_idx) { + logf!( + "run_task: return ra=0x%x sp=0x%x for caller %d", + caller_task.tf.regs[REG_RA], + caller_task.tf.regs[REG_SP], + caller_idx as u32 + ); + } + } + } + } // Prepare to enter user mode via sret: set sepc and clear sstatus.SPP. let mut sstatus: u32; unsafe { diff --git a/crates/kernel/src/task/task.rs b/crates/kernel/src/task/task.rs index 696d8e5..db0fa1d 100644 --- a/crates/kernel/src/task/task.rs +++ b/crates/kernel/src/task/task.rs @@ -53,6 +53,8 @@ pub struct Task { pub addr_space: AddressSpace, /// Next heap pointer for this task (virtual address). pub heap_ptr: u32, + /// Task slot that initiated this task, if any. + pub caller_task_id: Option, } impl Task { @@ -61,6 +63,7 @@ impl Task { tf: TrapFrame::default(), addr_space, heap_ptr, + caller_task_id: None, } } diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index b313d9f..9871518 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -1,6 +1,8 @@ use core::arch::asm; use program::{log, logf}; +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; +use crate::memory::page_allocator as mmu; use crate::syscall; use crate::task::TRAMPOLINE_VA; @@ -12,10 +14,12 @@ use save_trap_frame::save_trap_frame; const SCAUSE_ECALL_FROM_U: usize = 8; const SCAUSE_ECALL_FROM_S: usize = 9; +const SCAUSE_BREAKPOINT: usize = 3; const SSTATUS_SPP: u32 = 1 << 8; const REG_COUNT: usize = 32; const TRAP_FRAME_WORDS: usize = REG_COUNT + 1; // regs + pc const TRAP_FRAME_BYTES: i32 = (TRAP_FRAME_WORDS * 4) as i32; +const REG_RA: usize = 1; const REG_A0: usize = 10; const REG_A1: usize = 11; const REG_A2: usize = 12; @@ -24,6 +28,7 @@ const REG_A4: usize = 14; const REG_A5: usize = 15; const REG_A6: usize = 16; const REG_A7: usize = 17; +const REG_SP: usize = 2; const REG_PC: usize = 32; /// Install the kernel trap vector and set up the kernel stack for traps. @@ -48,8 +53,11 @@ pub unsafe extern "C" fn trap_entry() -> ! { core::arch::asm!( "call {swap} # switch to kernel stack and reserve trap frame", "call {save} # save regs on kernel stack", + "mv s0, a0 # preserve trap frame pointer across handle_trap", "call {handler} # run Rust trap handler", - "j {restore} # restore regs and return", + "mv a2, a1 # stash return kind", + "mv a1, s0 # restore trap frame pointer for restore", + "j {restore}", swap = sym swap_to_kernel_stack, save = sym save_trap_frame, handler = sym handle_trap, @@ -62,7 +70,12 @@ pub unsafe extern "C" fn trap_entry() -> ! { #[unsafe(naked)] unsafe extern "C" fn swap_to_kernel_stack() -> ! { core::arch::naked_asm!( + // Swap sp with sscratch: + // - On trap entry, sscratch holds the kernel stack top. + // - After the swap, sp points at the kernel stack and the previous sp + // (user sp) is saved in sscratch for later restoration. "csrrw sp, sscratch, sp", + // Reserve space for the trap frame on the kernel stack. "addi sp, sp, -{frame_bytes}", "ret", frame_bytes = const TRAP_FRAME_BYTES, @@ -88,7 +101,7 @@ unsafe extern "C" fn return_from_trap() -> ! { /// laid out as: /// regs[0..32] = x0..x31, regs[32] = pc. #[unsafe(no_mangle)] -pub extern "C" fn handle_trap(saved: *mut u32) { +pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { let regs = unsafe { core::slice::from_raw_parts_mut(saved, TRAP_FRAME_WORDS) }; let scause = read_scause(); let stval = read_stval(); @@ -103,6 +116,8 @@ pub extern "C" fn handle_trap(saved: *mut u32) { } let code = scause & 0xfff; + let mut return_kind = if read_sstatus() & SSTATUS_SPP != 0 { 1 } else { 0 }; + let mut return_sp = regs[REG_SP]; match code { SCAUSE_ECALL_FROM_U | SCAUSE_ECALL_FROM_S => { let args = [ @@ -122,9 +137,64 @@ pub extern "C" fn handle_trap(saved: *mut u32) { let ret = syscall::dispatch_syscall(call_id, args, caller_mode); regs[REG_A0] = ret; // a0 return value regs[REG_PC] = regs[REG_PC].wrapping_add(4); // Advance past ecall + return_kind = 0; + return_sp = regs[REG_SP]; + } + SCAUSE_BREAKPOINT => { + // Default to returning to the kernel task unless the current task has a caller. + let mut caller_idx = KERNEL_TASK_SLOT; + unsafe { + let current = *CURRENT_TASK.get_mut(); + let tasks = TASKS.get_mut(); + // If this is a user task, save its current trapframe so it can be resumed later. + if current != KERNEL_TASK_SLOT { + if let Some(task) = tasks.get_mut(current) { + for (idx, value) in regs.iter().take(REG_COUNT).enumerate() { + task.tf.regs[idx] = *value; + } + task.tf.pc = regs[REG_PC]; + // Use the recorded caller task as the return target. + caller_idx = task.caller_task_id.unwrap_or(KERNEL_TASK_SLOT); + } + } + // Restore the caller task's trapframe and address-space root. + if let Some(caller_task) = tasks.get(caller_idx) { + for (idx, value) in caller_task.tf.regs.iter().take(REG_COUNT).enumerate() { + regs[idx] = *value; + } + // Resume at the caller's return address. + regs[REG_PC] = caller_task.tf.regs[REG_RA]; + mmu::set_current_root(caller_task.addr_space.root_ppn); + return_sp = caller_task.tf.regs[REG_SP]; + logf!( + "breakpoint return: caller=%d pc=0x%x ra=0x%x sp=0x%x", + caller_idx as u32, + caller_task.tf.pc, + caller_task.tf.regs[REG_RA], + caller_task.tf.regs[REG_SP] + ); + } else { + panic!("breakpoint trap: caller task missing"); + } + // Mark the caller as the current task after the handoff. + *CURRENT_TASK.get_mut() = caller_idx; + } + let mut sstatus = read_sstatus(); + // Set SPP so sret returns to the correct privilege level. + if caller_idx == KERNEL_TASK_SLOT { + // Return to supervisor when the caller is the kernel task. + sstatus |= SSTATUS_SPP; + return_kind = 1; + } else { + // Clear SPP to return to user mode for user callers. + sstatus &= !SSTATUS_SPP; + return_kind = 0; + } + unsafe { asm!("csrw sstatus, {0}", in(reg) sstatus); } } _ => log!("unhandled trap"), } + (return_sp, return_kind) } #[inline(always)] diff --git a/crates/kernel/src/trap/restore_trap_frame.rs b/crates/kernel/src/trap/restore_trap_frame.rs index 9f497a2..67e9e54 100644 --- a/crates/kernel/src/trap/restore_trap_frame.rs +++ b/crates/kernel/src/trap/restore_trap_frame.rs @@ -1,45 +1,52 @@ use super::{return_from_trap, TRAP_FRAME_BYTES}; #[unsafe(naked)] -pub(super) unsafe extern "C" fn restore_trap_frame() -> ! { +pub(super) unsafe extern "C" fn restore_trap_frame( + return_sp: u32, /* used via a0 reg */ + frame_ptr: *const u32, +) -> ! { core::arch::naked_asm!( - // Restore sepc and registers. - "lw t1, 128(sp)", + // Restore sepc and registers using the provided trap-frame base (a1). + "lw t1, 128(a1)", "csrw sepc, t1", - "lw t2, 8(sp)", - "csrw sscratch, t2", // restore user sp for swap - "lw ra, 4(sp)", - "lw gp, 12(sp)", - "lw tp, 16(sp)", - "lw t0, 20(sp)", // user satp from trap trampoline - "lw s0, 32(sp)", - "lw s1, 36(sp)", - "lw a0, 40(sp)", - "lw a1, 44(sp)", - "lw a2, 48(sp)", - "lw a3, 52(sp)", - "lw a4, 56(sp)", - "lw a5, 60(sp)", - "lw a6, 64(sp)", - "lw a7, 68(sp)", - "lw s2, 72(sp)", - "lw s3, 76(sp)", - "lw s4, 80(sp)", - "lw s5, 84(sp)", - "lw s6, 88(sp)", - "lw s7, 92(sp)", - "lw s8, 96(sp)", - "lw s9, 100(sp)", - "lw s10, 104(sp)", - "lw s11, 108(sp)", - "lw t3, 112(sp)", - "lw t4, 116(sp)", - "lw t5, 120(sp)", - "lw t6, 124(sp)", - "lw t1, 24(sp)", - "lw t2, 28(sp)", - "addi sp, sp, {frame_bytes}", - "csrrw sp, sscratch, sp", + "beqz a2, 1f", + "csrw sscratch, a0", // kernel return: keep kernel sp for subsequent traps + "j 2f", + "1:", + "addi t1, a1, {frame_bytes}", + "csrw sscratch, t1", // user return: stash kernel sp for the next trap + "2:", + "lw ra, 4(a1)", + "lw gp, 12(a1)", + "lw tp, 16(a1)", + "lw t0, 20(a1)", // user satp from trap trampoline + "lw t1, 24(a1)", + "lw t2, 28(a1)", + "lw s0, 32(a1)", + "lw s1, 36(a1)", + "lw a2, 48(a1)", + "lw a3, 52(a1)", + "lw a4, 56(a1)", + "lw a5, 60(a1)", + "lw a6, 64(a1)", + "lw a7, 68(a1)", + "lw s2, 72(a1)", + "lw s3, 76(a1)", + "lw s4, 80(a1)", + "lw s5, 84(a1)", + "lw s6, 88(a1)", + "lw s7, 92(a1)", + "lw s8, 96(a1)", + "lw s9, 100(a1)", + "lw s10, 104(a1)", + "lw s11, 108(a1)", + "lw t3, 112(a1)", + "lw t4, 116(a1)", + "lw t5, 120(a1)", + "lw t6, 124(a1)", + "mv sp, a0", // restore caller-selected sp (user or kernel), not sscratch + "lw a0, 40(a1)", + "lw a1, 44(a1)", "j {return}", return = sym return_from_trap, frame_bytes = const TRAP_FRAME_BYTES, diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index bca8ebf..81543c8 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -19,6 +19,7 @@ pub const CSR_SCAUSE: u16 = 0x142; pub const CSR_STVAL: u16 = 0x143; const SCAUSE_ECALL_FROM_U: u32 = 8; const SCAUSE_ECALL_FROM_S: u32 = 9; +const SCAUSE_BREAKPOINT: u32 = 3; const SSTATUS_SPP: u32 = 1 << 8; #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index b56fb41..8c20b00 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,4 +1,4 @@ -use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP, CSR_SEPC, SCAUSE_ECALL_FROM_U}; +use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP, CSR_SEPC, SCAUSE_BREAKPOINT}; use crate::sys_call::SYSCALL_LOG; use crate::memory::VirtualAddress; use crate::host_interface::HostInterface; @@ -877,6 +877,12 @@ impl CPU { Instruction::Ebreak => { // EDUCATIONAL: EBREAK - Environment Break - for debugging // In real systems, this would trigger a debugger breakpoint + if self.priv_mode == super::PrivilegeMode::User && self.has_trap_vector() { + if !self.trap_to_vector(SCAUSE_BREAKPOINT, 0, None) { + panic!("trap_to_vector returned false for ebreak pc=0x{:08x}", self.pc); + } + return true; + } return false; } Instruction::Mret => { From 5b507f1e8b70994a7ee9e19b4f4889adf35196c2 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 30 Dec 2025 15:59:51 +0200 Subject: [PATCH 46/70] Refactor bundle resume flow - decode bundle into globals and process one tx at a time - add resume_bundle hook for program calls - wire main to decode then process - log erc20 transfer amount --- crates/examples/src/erc20.rs | 1 + crates/kernel/src/bundle/mod.rs | 83 ++++++++++++++++++++---- crates/kernel/src/bundle/program_call.rs | 7 +- crates/kernel/src/lib.rs | 8 ++- crates/kernel/src/main.rs | 8 ++- 5 files changed, 87 insertions(+), 20 deletions(-) diff --git a/crates/examples/src/erc20.rs b/crates/examples/src/erc20.rs index fe7c58c..9d00a44 100644 --- a/crates/examples/src/erc20.rs +++ b/crates/examples/src/erc20.rs @@ -87,6 +87,7 @@ fn mint(program: &Address, caller: Address, val: u32) { } fn transfer(program: &Address, caller: Address, to: Address, amount: u32) { + logf!("erc20: transfer amount=%d", amount); let from_bal = match Balances::get(program, caller) { O::Some(bal) => bal, O::None => 0, diff --git a/crates/kernel/src/bundle/mod.rs b/crates/kernel/src/bundle/mod.rs index 5a5d216..86eb253 100644 --- a/crates/kernel/src/bundle/mod.rs +++ b/crates/kernel/src/bundle/mod.rs @@ -3,36 +3,91 @@ use core::mem::forget; use program::{log, logf}; use types::transaction::{Transaction, TransactionBundle, TransactionType}; +use kernel::global::Global; + mod create_account; mod program_call; use self::create_account::create_account; use self::program_call::program_call; -pub(crate) fn process_bundle(encoded_bundle: &[u8]) { +static BUNDLE: Global> = Global::new(None); +static CURRNET_BUNDLE_TX: Global = Global::new(0); + +pub(crate) fn decode_bundle(encoded_bundle: &[u8]) -> bool { log!("processing transaction bundle"); if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { let count = bundle.transactions.len(); logf!("decoded tx count=%d", count as u32); - for i in 0..count { - logf!("processing tx %d/%d", (i + 1) as u32, count as u32); - if let Some(tx) = bundle.transactions.get(i) { - execute_transaction(tx); - } else { - logf!("missing tx at index %d", i as u32); - } + unsafe { + *BUNDLE.get_mut() = Some(bundle); + *CURRNET_BUNDLE_TX.get_mut() = 0; + } + true + } else { + false + } +} + +pub(crate) fn process_bundle() { + let (idx, count) = unsafe { + let count = BUNDLE + .get_mut() + .as_ref() + .map(|bundle| bundle.transactions.len()) + .unwrap_or(0); + (*CURRNET_BUNDLE_TX.get_mut(), count) + }; + if idx >= count { + bundle_complete(); + } + logf!("processing tx %d/%d", (idx + 1) as u32, count as u32); + let tx = unsafe { + BUNDLE + .get_mut() + .as_ref() + .and_then(|bundle| bundle.transactions.get(idx)) + }; + if let Some(tx) = tx { + if execute_transaction(tx) { + resume_bundle(); } - // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. - forget(bundle); } else { - log!("bundle decode failed"); + logf!("missing tx at index %d", idx as u32); + resume_bundle(); } } -fn execute_transaction(tx: &Transaction) { +pub(crate) extern "C" fn resume_bundle() -> ! { + unsafe { + let curr = *CURRNET_BUNDLE_TX.get_mut(); + *CURRNET_BUNDLE_TX.get_mut() = curr.wrapping_add(1); + } + process_bundle(); + loop {} +} + +fn execute_transaction(tx: &Transaction) -> bool { match tx.tx_type { - TransactionType::CreateAccount => create_account(tx), - TransactionType::ProgramCall => program_call(tx), + TransactionType::CreateAccount => { + create_account(tx); + true + } + TransactionType::ProgramCall => { + program_call(tx, resume_bundle); + false + } _ => panic!("unsupported transaction type"), } } + +fn bundle_complete() -> ! { + log!("transaction bundle complete"); + // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. + let bundle = unsafe { BUNDLE.get_mut().take() }; + if let Some(bundle) = bundle { + forget(bundle); + } + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/bundle/program_call.rs b/crates/kernel/src/bundle/program_call.rs index c901cff..b53d3a1 100644 --- a/crates/kernel/src/bundle/program_call.rs +++ b/crates/kernel/src/bundle/program_call.rs @@ -7,7 +7,7 @@ use program::parser::HexCodec; use state::State; use types::transaction::Transaction; -pub(crate) fn program_call(tx: &Transaction) { +pub(crate) fn program_call(tx: &Transaction, resume: extern "C" fn() -> !) { let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; let account = match state.get_account(&tx.to) { Some(acc) => acc, @@ -90,11 +90,12 @@ pub(crate) fn program_call(tx: &Transaction) { } let current = tasks_slot.len().saturating_sub(1); core::arch::asm!( - "la ra, 1f", + "mv ra, {resume}", "j {run}", - "1:", run = sym kernel_run_task, + resume = in(reg) resume as usize, in("a0") current, + options(noreturn), ); } } else { diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index fffe9ce..abf1e04 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -8,7 +8,13 @@ pub use types::boot::BootInfo; pub mod global; pub mod task; pub use task::{AddressSpace, Task, TrapFrame}; -pub use task::{kernel_run_task, prep_program_task, run_task, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; +pub use task::{ + kernel_run_task, + prep_program_task, + run_task, + PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES, +}; pub mod memory; pub mod trap; pub mod syscall; diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index 484d0ed..eeac708 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -9,7 +9,7 @@ use program::{log, logf}; mod init; mod bundle; -use crate::bundle::process_bundle; +use crate::bundle::{decode_bundle, process_bundle}; use crate::init::init_kernel; #[allow(dead_code)] @@ -32,7 +32,11 @@ pub extern "C" fn _start( init_kernel(state_ptr, state_len, boot_info_ptr); let encoded_bundle = unsafe { slice::from_raw_parts(bundle_ptr, bundle_len) }; - process_bundle(encoded_bundle); + if decode_bundle(encoded_bundle) { + process_bundle(); + } else { + log!("bundle decode failed"); + } log!("finished bundle execution"); halt(); From 8b08cc0ccf0d97155cec6e0655fad149a5d85132 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 30 Dec 2025 17:12:42 +0200 Subject: [PATCH 47/70] Move receipts to types and wire kernel fire-event - add TransactionReceipt to types and re-export in the types lib - track bundle receipts and current tx in kernel globals - initialize receipts when decoding bundles and keep them through bundle execution - implement sys_fire_event in its own module and append events to current receipt - reuse existing user-memory helpers for event payload reads --- crates/kernel/src/bundle/mod.rs | 28 ++++++++--- crates/kernel/src/global.rs | 8 +++ crates/kernel/src/syscall/fire_event.rs | 34 +++++++++++++ crates/kernel/src/syscall/mod.rs | 6 +-- crates/kernel/src/syscall/storage.rs | 4 +- crates/types/src/lib.rs | 3 ++ crates/types/src/receipt.rs | 66 +++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 crates/kernel/src/syscall/fire_event.rs create mode 100644 crates/types/src/receipt.rs diff --git a/crates/kernel/src/bundle/mod.rs b/crates/kernel/src/bundle/mod.rs index 86eb253..afd8136 100644 --- a/crates/kernel/src/bundle/mod.rs +++ b/crates/kernel/src/bundle/mod.rs @@ -1,9 +1,13 @@ use core::mem::forget; +extern crate alloc; + +use alloc::vec::Vec; use program::{log, logf}; use types::transaction::{Transaction, TransactionBundle, TransactionType}; +use types::{Result, TransactionReceipt}; -use kernel::global::Global; +use kernel::global::{BUNDLE, CURRENT_TX, RECEIPTS}; mod create_account; mod program_call; @@ -11,17 +15,21 @@ mod program_call; use self::create_account::create_account; use self::program_call::program_call; -static BUNDLE: Global> = Global::new(None); -static CURRNET_BUNDLE_TX: Global = Global::new(0); - pub(crate) fn decode_bundle(encoded_bundle: &[u8]) -> bool { log!("processing transaction bundle"); if let Some(bundle) = TransactionBundle::decode(encoded_bundle) { let count = bundle.transactions.len(); logf!("decoded tx count=%d", count as u32); + let receipts = bundle + .transactions + .iter() + .cloned() + .map(|tx| TransactionReceipt::new(tx, Result::new(true, 0))) + .collect::>(); unsafe { *BUNDLE.get_mut() = Some(bundle); - *CURRNET_BUNDLE_TX.get_mut() = 0; + *CURRENT_TX.get_mut() = 0; + *RECEIPTS.get_mut() = Some(receipts); } true } else { @@ -36,7 +44,7 @@ pub(crate) fn process_bundle() { .as_ref() .map(|bundle| bundle.transactions.len()) .unwrap_or(0); - (*CURRNET_BUNDLE_TX.get_mut(), count) + (*CURRENT_TX.get_mut(), count) }; if idx >= count { bundle_complete(); @@ -60,8 +68,8 @@ pub(crate) fn process_bundle() { pub(crate) extern "C" fn resume_bundle() -> ! { unsafe { - let curr = *CURRNET_BUNDLE_TX.get_mut(); - *CURRNET_BUNDLE_TX.get_mut() = curr.wrapping_add(1); + let curr = *CURRENT_TX.get_mut(); + *CURRENT_TX.get_mut() = curr.wrapping_add(1); } process_bundle(); loop {} @@ -88,6 +96,10 @@ fn bundle_complete() -> ! { if let Some(bundle) = bundle { forget(bundle); } + let receipts = unsafe { RECEIPTS.get_mut().take() }; + if let Some(receipts) = receipts { + forget(receipts); + } unsafe { core::arch::asm!("ebreak") }; loop {} } diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index a07c9c9..2a1a21d 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -1,7 +1,12 @@ +extern crate alloc; + +use alloc::vec::Vec; use core::cell::UnsafeCell; use core::mem::MaybeUninit; use core::ptr; use state::State; +use types::TransactionReceipt; +use types::transaction::TransactionBundle; use crate::Task; use crate::memory::heap::BumpAllocator; @@ -33,6 +38,9 @@ unsafe impl Sync for Global {} pub const MAX_TASKS: usize = 16; pub const KERNEL_TASK_SLOT: usize = 0; pub static CURRENT_TASK: Global = Global::new(KERNEL_TASK_SLOT); +pub static CURRENT_TX: Global = Global::new(0); +pub static RECEIPTS: Global>> = Global::new(None); +pub static BUNDLE: Global> = Global::new(None); pub struct TaskList { len: usize, diff --git a/crates/kernel/src/syscall/fire_event.rs b/crates/kernel/src/syscall/fire_event.rs new file mode 100644 index 0000000..d8d0c33 --- /dev/null +++ b/crates/kernel/src/syscall/fire_event.rs @@ -0,0 +1,34 @@ +use program::logf; + +use crate::global::{CURRENT_TX, RECEIPTS}; +use crate::syscall::storage::{current_root_ppn, read_user_bytes}; + +pub(crate) fn sys_fire_event(args: [u32; 6]) -> u32 { + let ptr = args[0]; + let len = args[1] as usize; + + let root_ppn = match current_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let event_bytes = match read_user_bytes(root_ppn, ptr, len) { + Some(bytes) => bytes, + None => return 0, + }; + + let current_idx = unsafe { *CURRENT_TX.get_mut() }; + let receipts = unsafe { RECEIPTS.get_mut() }; + match receipts + .as_mut() + .and_then(|receipts| receipts.get_mut(current_idx)) + { + Some(receipt) => { + receipt.add_event(event_bytes); + } + None => { + logf!("sys_fire_event: missing receipt for tx %d", current_idx as u32); + } + } + 0 +} diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 6402a76..6d37b2a 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -4,10 +4,12 @@ use program::{log, logf}; pub mod alloc; +pub mod fire_event; pub mod panic; pub mod storage; use alloc::{sys_alloc, sys_dealloc}; +use fire_event::sys_fire_event; use panic::sys_panic; use storage::{sys_storage_get, sys_storage_set}; pub(crate) use panic::sys_panic_with_message; @@ -64,10 +66,6 @@ fn sys_call_program(_args: [u32; 6]) -> u32 { 0 } -fn sys_fire_event(_args: [u32; 6]) -> u32 { - log!("sys_fire_event: need implementation"); - 0 -} fn sys_transfer(_args: [u32; 6]) -> u32 { log!("sys_transfer: need implementation"); diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index 5db4564..887b250 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -155,7 +155,7 @@ pub(crate) fn sys_storage_set(args: [u32; 6]) -> u32 { 0 } -fn current_root_ppn() -> Option { +pub(crate) fn current_root_ppn() -> Option { let current = unsafe { *CURRENT_TASK.get_mut() }; let tasks = unsafe { TASKS.get_mut() }; match tasks.get(current) { @@ -167,7 +167,7 @@ fn current_root_ppn() -> Option { } } -fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option> { +pub(crate) fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option> { if len == 0 { return Some(Vec::new()); } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 50e530e..ced2ac5 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -18,6 +18,9 @@ pub use primitives::*; pub mod transaction; pub use transaction::*; +pub mod receipt; +pub use receipt::TransactionReceipt; + pub mod boot; pub use boot::BootInfo; diff --git a/crates/types/src/receipt.rs b/crates/types/src/receipt.rs new file mode 100644 index 0000000..03fc44b --- /dev/null +++ b/crates/types/src/receipt.rs @@ -0,0 +1,66 @@ +extern crate alloc; + +use alloc::vec::Vec; +use core::fmt; + +use crate::result::Result; +use crate::transaction::Transaction; + +/// Represents the result of a transaction execution. +#[derive(Debug, Clone)] +pub struct TransactionReceipt { + /// Hash of the transaction. + pub tx: Transaction, + + /// Result status and optional data. + pub result: Result, + + /// List of log entries generated during execution. + pub events: Vec>, +} + +impl TransactionReceipt { + /// Creates a new TransactionReceipt. + pub fn new(tx: Transaction, result: Result) -> Self { + TransactionReceipt { + tx, + result, + events: Vec::new(), + } + } + + /// Adds an event to the receipt. + pub fn add_event(&mut self, event: Vec) -> &TransactionReceipt { + self.events.push(event); + self + } + + /// Optionally add multiple events at once. + pub fn set_events(mut self, events: Vec>) -> Self { + self.events = events; + self + } +} + +impl fmt::Display for TransactionReceipt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "=== Transaction Receipt ===")?; + writeln!(f, "From: {:?}", self.tx.from)?; + writeln!(f, "To: {:?}", self.tx.to)?; + writeln!(f, "Result: {:?}", self.result)?; + writeln!(f, "Events:")?; + + for (i, event) in self.events.iter().enumerate() { + write!(f, " [{}] ", i)?; + for (j, byte) in event.iter().enumerate() { + if j > 0 { + write!(f, " ")?; + } + write!(f, "{:02x}", byte)?; + } + writeln!(f)?; + } + + Ok(()) + } +} From 8050a7630e2eed0af5a6f811b2476d5f00675323 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 30 Dec 2025 18:57:53 +0200 Subject: [PATCH 48/70] Add kernel receipts handoff and receipt assertions - capture program results in tasks and attach them to receipts - serialize receipts to kernel memory with a fixed handoff header - read receipts back in the bootloader and print/verify in tests - add receipt encode/decode helpers and kernel result type - refactor receipt handoff helpers into result modules --- crates/bootloader/src/bootloader.rs | 6 +- crates/bootloader/src/lib.rs | 1 + crates/bootloader/src/result.rs | 29 ++++ crates/examples/tests/common/test_runner.rs | 65 ++++++++- crates/kernel/src/bundle/mod.rs | 4 + crates/kernel/src/bundle/result.rs | 76 ++++++++++ crates/kernel/src/config.rs | 2 + crates/kernel/src/global.rs | 9 ++ crates/kernel/src/task/task.rs | 4 + crates/kernel/src/trap/mod.rs | 52 ++++++- crates/types/src/kernel_result.rs | 12 ++ crates/types/src/lib.rs | 3 + crates/types/src/receipt.rs | 146 ++++++++++++++++++++ 13 files changed, 405 insertions(+), 4 deletions(-) create mode 100644 crates/bootloader/src/result.rs create mode 100644 crates/kernel/src/bundle/result.rs create mode 100644 crates/types/src/kernel_result.rs diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index f7ae54b..3dda445 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -6,7 +6,7 @@ use std::vec::Vec; use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; -use types::{boot::BootInfo, transaction::TransactionBundle, SV32_DIRECT_MAP_BASE}; +use types::{boot::BootInfo, transaction::TransactionBundle, TransactionReceipt, SV32_DIRECT_MAP_BASE}; use crate::DefaultSyscallHandler; use state::State; @@ -140,7 +140,7 @@ impl Bootloader { state: Rc>, verbose: bool, verbose_writer: Option>>, - ) { + ) -> Option> { let (entry_point, memory) = self.load_kernel(kernel_elf); let host: Box = Box::new(NoopHost); @@ -164,6 +164,7 @@ impl Bootloader { self.place_state(&mut vm, &encoded_state); self.place_boot_info(&mut vm); vm.raw_run(); + crate::result::read_kernel_receipts(&memory) } fn place_bundle(&mut self, vm: &mut VM, bundle: &TransactionBundle) { @@ -246,4 +247,5 @@ impl Bootloader { self.heap_ptr.set(end); start } + } diff --git a/crates/bootloader/src/lib.rs b/crates/bootloader/src/lib.rs index d73296f..22bb9b3 100644 --- a/crates/bootloader/src/lib.rs +++ b/crates/bootloader/src/lib.rs @@ -8,6 +8,7 @@ //! Memory utilities are provided by the VM crate. pub mod bootloader; +pub mod result; pub mod syscalls; diff --git a/crates/bootloader/src/result.rs b/crates/bootloader/src/result.rs new file mode 100644 index 0000000..e483747 --- /dev/null +++ b/crates/bootloader/src/result.rs @@ -0,0 +1,29 @@ +use core::mem; + +use types::kernel_result::KERNEL_RESULT_ADDR; +use types::{KernelResult, TransactionReceipt}; +use vm::memory::{Memory as MmuRef, VirtualAddress}; + +pub(crate) fn read_kernel_receipts(memory: &MmuRef) -> Option> { + let header_size = mem::size_of::() as u32; + let header_end = KERNEL_RESULT_ADDR.checked_add(header_size)?; + let header_slice = memory.mem_slice( + VirtualAddress(KERNEL_RESULT_ADDR), + VirtualAddress(header_end), + )?; + let header_bytes = header_slice.as_ref(); + if header_bytes.len() < header_size as usize { + return None; + } + let receipts_ptr = u32::from_le_bytes(header_bytes[0..4].try_into().ok()?); + let receipts_len = u32::from_le_bytes(header_bytes[4..8].try_into().ok()?); + if receipts_ptr == 0 || receipts_len == 0 { + return None; + } + let receipts_end = receipts_ptr.checked_add(receipts_len)?; + let receipts_slice = memory.mem_slice( + VirtualAddress(receipts_ptr), + VirtualAddress(receipts_end), + )?; + TransactionReceipt::decode_list(receipts_slice.as_ref()) +} diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index 30a63c2..7dace42 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -149,7 +149,7 @@ impl TestRunner { writeln!(self.writer.borrow_mut()).unwrap(); // Execute the whole bundle via the bootloader/kernel path. - bootloader.execute_bundle( + let receipts = bootloader.execute_bundle( self.kernel_bytes.as_ref().ok_or_else(|| { "KERNEL_ELF not set or unreadable; bootloader path required".to_string() })?, @@ -163,6 +163,69 @@ impl TestRunner { }, ); + let receipts = match receipts { + Some(receipts) => receipts, + None => { + return Err("Bootloader returned no receipts".to_string()); + } + }; + + let receipt = receipts + .last() + .ok_or_else(|| "No receipts returned from kernel".to_string())?; + writeln!(self.writer.borrow_mut(), "\n=== Receipt ===").unwrap(); + writeln!(self.writer.borrow_mut(), "{receipt}").unwrap(); + let result = receipt.result; + if result.success != case.expected_success { + return Err(format!( + "Expected success={}, got {}", + case.expected_success, result.success + )); + } + let error_code = result.error_code; + if error_code != case.expected_error_code { + return Err(format!( + "Expected error_code={}, got {}", + case.expected_error_code, error_code + )); + } + match &case.expected_data { + Some(expected) => { + let expected_len = expected.len(); + let data_len = result.data_len as usize; + let actual_len = data_len; + if actual_len != expected_len { + return Err(format!( + "Expected data_len={}, got {}", + expected_len, actual_len + )); + } + let actual = &result.data[..actual_len.min(result.data.len())]; + if actual != expected.as_slice() { + return Err(format!( + "Expected data {:?}, got {:?}", + expected, actual + )); + } + } + None => { + let data_len = result.data_len; + if data_len != 0 { + return Err(format!( + "Expected empty data, got data_len={}", + data_len + )); + } + } + } + + writeln!( + self.writer.borrow_mut(), + "✅ Test case '{}' passed", + case.name + ) + .unwrap(); + // For now we treat successful bootloader execution as a passed test. Ok(()) } diff --git a/crates/kernel/src/bundle/mod.rs b/crates/kernel/src/bundle/mod.rs index afd8136..92def24 100644 --- a/crates/kernel/src/bundle/mod.rs +++ b/crates/kernel/src/bundle/mod.rs @@ -11,9 +11,11 @@ use kernel::global::{BUNDLE, CURRENT_TX, RECEIPTS}; mod create_account; mod program_call; +mod result; use self::create_account::create_account; use self::program_call::program_call; +use self::result::{update_receipt_from_task, write_kernel_result}; pub(crate) fn decode_bundle(encoded_bundle: &[u8]) -> bool { log!("processing transaction bundle"); @@ -67,6 +69,7 @@ pub(crate) fn process_bundle() { } pub(crate) extern "C" fn resume_bundle() -> ! { + update_receipt_from_task(); unsafe { let curr = *CURRENT_TX.get_mut(); *CURRENT_TX.get_mut() = curr.wrapping_add(1); @@ -91,6 +94,7 @@ fn execute_transaction(tx: &Transaction) -> bool { fn bundle_complete() -> ! { log!("transaction bundle complete"); + write_kernel_result(); // Avoid drop-time teardown that can allocate/deallocate; we halt immediately. let bundle = unsafe { BUNDLE.get_mut().take() }; if let Some(bundle) = bundle { diff --git a/crates/kernel/src/bundle/result.rs b/crates/kernel/src/bundle/result.rs new file mode 100644 index 0000000..4e1589e --- /dev/null +++ b/crates/kernel/src/bundle/result.rs @@ -0,0 +1,76 @@ +use kernel::Config; +use kernel::global::{CURRENT_TX, LAST_COMPLETED_TASK, RECEIPTS, TASKS}; +use program::{log, logf}; +use types::{KernelResult, TransactionReceipt}; + +pub(crate) fn update_receipt_from_task() { + let (tx_idx, task_idx) = unsafe { + let tx_idx = *CURRENT_TX.get_mut(); + let task_idx = (*LAST_COMPLETED_TASK.get_mut()).take(); + (tx_idx, task_idx) + }; + let task_idx = match task_idx { + Some(idx) => idx, + None => { + log!("resume_bundle: no completed task to update receipt"); + return; + } + }; + let result = unsafe { + let tasks = TASKS.get_mut(); + tasks + .get(task_idx) + .and_then(|task| task.last_result) + }; + let result = match result { + Some(res) => res, + None => { + log!("resume_bundle: completed task missing result"); + return; + } + }; + unsafe { + if let Some(receipts) = RECEIPTS.get_mut().as_mut() { + if let Some(receipt) = receipts.get_mut(tx_idx) { + receipt.result = result; + } else { + logf!("resume_bundle: invalid receipt index %d", tx_idx as u32); + } + } else { + log!("resume_bundle: receipts missing"); + } + } +} + +pub(crate) fn write_kernel_result() { + let encoded = unsafe { + RECEIPTS + .get_mut() + .as_ref() + .map(|receipts| TransactionReceipt::encode_list(receipts)) + }; + let encoded = match encoded { + Some(data) => data, + None => { + log!("kernel_result: receipts missing"); + return; + } + }; + let len = encoded.len() as u32; + // The bootloader maps the kernel window at VA 0, so this VA is also a + // physical address in the current setup. + let ptr = encoded.as_ptr() as u32; + core::mem::forget(encoded); + let header = KernelResult { + receipts_ptr: ptr, + receipts_len: len, + }; + unsafe { + core::ptr::write_volatile(Config::KERNEL_RESULT_ADDR as *mut KernelResult, header); + } + logf!( + "kernel_result: receipts_ptr=0x%x receipts_len=%d", + ptr, + len + ); +} diff --git a/crates/kernel/src/config.rs b/crates/kernel/src/config.rs index e39b61b..b71a1d8 100644 --- a/crates/kernel/src/config.rs +++ b/crates/kernel/src/config.rs @@ -9,4 +9,6 @@ impl Config { pub const PROGRAM_START_ADDR: u32 = 0x400; pub const RESULT_ADDR: u32 = 0x100; + /// Kernel handoff header address for serialized receipts (kernel VA). + pub const KERNEL_RESULT_ADDR: u32 = 0x100; } diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 2a1a21d..683b0aa 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -35,11 +35,20 @@ impl Global { unsafe impl Sync for Global {} +/// Max number of task slots the kernel tracks at once. pub const MAX_TASKS: usize = 16; +/// Reserved slot index for the kernel/supervisor task. pub const KERNEL_TASK_SLOT: usize = 0; +/// Currently running task slot index (kernel or user). pub static CURRENT_TASK: Global = Global::new(KERNEL_TASK_SLOT); +/// Index of the bundle transaction currently being executed. pub static CURRENT_TX: Global = Global::new(0); +/// Task slot that most recently completed and returned to the kernel. +/// Used to attach the correct program result to the current receipt. +pub static LAST_COMPLETED_TASK: Global> = Global::new(None); +/// Active receipts buffer being filled while processing a bundle. pub static RECEIPTS: Global>> = Global::new(None); +/// Currently decoded bundle, if any. pub static BUNDLE: Global> = Global::new(None); pub struct TaskList { diff --git a/crates/kernel/src/task/task.rs b/crates/kernel/src/task/task.rs index db0fa1d..c89f5f2 100644 --- a/crates/kernel/src/task/task.rs +++ b/crates/kernel/src/task/task.rs @@ -1,4 +1,5 @@ use core::fmt; +use types::result::Result as VmResult; /// Minimal trapframe capturing user-visible registers on trap/return. /// This mirrors RISC-V general-purpose regs plus PC. @@ -55,6 +56,8 @@ pub struct Task { pub heap_ptr: u32, /// Task slot that initiated this task, if any. pub caller_task_id: Option, + /// Last decoded program result for this task, if any. + pub last_result: Option, } impl Task { @@ -64,6 +67,7 @@ impl Task { addr_space, heap_ptr, caller_task_id: None, + last_result: None, } } diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index 9871518..5b9ff01 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -1,10 +1,13 @@ use core::arch::asm; use program::{log, logf}; +use types::result::{Result as VmResult, RESULT_DATA_SIZE}; -use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, LAST_COMPLETED_TASK, TASKS}; use crate::memory::page_allocator as mmu; use crate::syscall; +use crate::syscall::storage::read_user_bytes; use crate::task::TRAMPOLINE_VA; +use crate::{Config, Task}; mod save_trap_frame; mod restore_trap_frame; @@ -149,12 +152,23 @@ pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { // If this is a user task, save its current trapframe so it can be resumed later. if current != KERNEL_TASK_SLOT { if let Some(task) = tasks.get_mut(current) { + if let Some(result) = read_task_result(task) { + task.last_result = Some(result); + log_task_result(&result); + } else { + log!("program result: failed to read result bytes"); + } for (idx, value) in regs.iter().take(REG_COUNT).enumerate() { task.tf.regs[idx] = *value; } task.tf.pc = regs[REG_PC]; // Use the recorded caller task as the return target. caller_idx = task.caller_task_id.unwrap_or(KERNEL_TASK_SLOT); + if caller_idx == KERNEL_TASK_SLOT { + // Only record tasks that return to the kernel so bundle resume can + // associate the completed task with the current transaction receipt. + *LAST_COMPLETED_TASK.get_mut() = Some(current); + } } } // Restore the caller task's trapframe and address-space root. @@ -218,6 +232,42 @@ fn read_sstatus() -> u32 { value } +fn read_task_result(task: &Task) -> Option { + let result_bytes = + read_user_bytes(task.addr_space.root_ppn, Config::RESULT_ADDR, Config::MAX_RESULT_SIZE)?; + if result_bytes.len() < 9 { + return None; + } + let success = result_bytes[0] != 0; + let error_code = u32::from_le_bytes(result_bytes[1..5].try_into().ok()?); + let data_len = u32::from_le_bytes(result_bytes[5..9].try_into().ok()?); + let data_len = (data_len as usize).min(RESULT_DATA_SIZE); + if result_bytes.len() < 9 + data_len { + return None; + } + let mut data = [0u8; RESULT_DATA_SIZE]; + data[..data_len].copy_from_slice(&result_bytes[9..9 + data_len]); + Some(VmResult { + success, + error_code, + data_len: data_len as u32, + data, + }) +} + +fn log_task_result(result: &VmResult) { + let data_len = (result.data_len as usize).min(RESULT_DATA_SIZE); + logf!( + "program result: success=%d error=%d data_len=%d", + result.success as u32, + result.error_code, + data_len as u32 + ); + if data_len > 0 { + log!("program result data: %b", &result.data[..data_len]); + } +} + #[inline(always)] fn read_stval() -> usize { let value: usize; diff --git a/crates/types/src/kernel_result.rs b/crates/types/src/kernel_result.rs new file mode 100644 index 0000000..df1aacf --- /dev/null +++ b/crates/types/src/kernel_result.rs @@ -0,0 +1,12 @@ +//! Kernel-to-bootloader handoff header for serialized receipts. + +/// Pointer + length describing the receipts buffer in kernel memory. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +pub struct KernelResult { + pub receipts_ptr: u32, + pub receipts_len: u32, +} + +/// Kernel VA where the handoff header is written. +pub const KERNEL_RESULT_ADDR: u32 = 0x100; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index ced2ac5..178b01b 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -21,6 +21,9 @@ pub use transaction::*; pub mod receipt; pub use receipt::TransactionReceipt; +pub mod kernel_result; +pub use kernel_result::KernelResult; + pub mod boot; pub use boot::BootInfo; diff --git a/crates/types/src/receipt.rs b/crates/types/src/receipt.rs index 03fc44b..d8a4a58 100644 --- a/crates/types/src/receipt.rs +++ b/crates/types/src/receipt.rs @@ -1,6 +1,7 @@ extern crate alloc; use alloc::vec::Vec; +use core::convert::TryInto; use core::fmt; use crate::result::Result; @@ -40,6 +41,151 @@ impl TransactionReceipt { self.events = events; self } + + /// Encode this receipt into a flat little-endian buffer. + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + out.push(self.tx.tx_type as u8); + out.extend_from_slice(&self.tx.to.0); + out.extend_from_slice(&self.tx.from.0); + out.extend_from_slice(&(self.tx.data.len() as u32).to_le_bytes()); + out.extend_from_slice(&self.tx.data); + out.extend_from_slice(&self.tx.value.to_le_bytes()); + out.extend_from_slice(&self.tx.nonce.to_le_bytes()); + + out.push(self.result.success as u8); + out.extend_from_slice(&self.result.error_code.to_le_bytes()); + out.extend_from_slice(&self.result.data_len.to_le_bytes()); + let data_len = self.result.data_len as usize; + out.extend_from_slice(&self.result.data[..data_len.min(self.result.data.len())]); + + out.extend_from_slice(&(self.events.len() as u32).to_le_bytes()); + for event in &self.events { + out.extend_from_slice(&(event.len() as u32).to_le_bytes()); + out.extend_from_slice(event); + } + + out + } + + /// Decode a receipt from a buffer, returning the receipt and bytes consumed. + pub fn decode(encoded: &[u8]) -> Option<(Self, usize)> { + let mut cursor = 0usize; + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + + let tx_type = *read(1)?.first()?; + let tx_type = crate::transaction::TransactionType::from_u8(tx_type)?; + + let mut to = [0u8; 20]; + to.copy_from_slice(read(20)?); + let mut from = [0u8; 20]; + from.copy_from_slice(read(20)?); + + let data_len = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let data = read(data_len)?.to_vec(); + + let value = + u64::from_le_bytes(read(8)?.try_into().ok()?); + let nonce = + u64::from_le_bytes(read(8)?.try_into().ok()?); + + let success = *read(1)?.first()? != 0; + let error_code = + u32::from_le_bytes(read(4)?.try_into().ok()?); + let result_data_len = + u32::from_le_bytes(read(4)?.try_into().ok()?); + let result_len = result_data_len as usize; + let result_data = read(result_len)?; + + let mut data_buf = [0u8; crate::result::RESULT_DATA_SIZE]; + let copy_len = result_len.min(data_buf.len()); + data_buf[..copy_len].copy_from_slice(&result_data[..copy_len]); + + let mut result = Result { + success, + error_code, + data_len: result_data_len, + data: data_buf, + }; + if result.data_len as usize > crate::result::RESULT_DATA_SIZE { + result.data_len = crate::result::RESULT_DATA_SIZE as u32; + } + + let event_count = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let mut events = Vec::with_capacity(event_count); + for _ in 0..event_count { + let len = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let bytes = read(len)?.to_vec(); + events.push(bytes); + } + + let tx = Transaction { + tx_type, + to: crate::address::Address(to), + from: crate::address::Address(from), + data, + value, + nonce, + }; + + Some(( + TransactionReceipt { + tx, + result, + events, + }, + cursor, + )) + } + + /// Encode a receipts list with a count prefix and per-receipt length. + pub fn encode_list(receipts: &[TransactionReceipt]) -> Vec { + let mut out = Vec::new(); + out.extend_from_slice(&(receipts.len() as u32).to_le_bytes()); + for receipt in receipts { + let encoded = receipt.encode(); + out.extend_from_slice(&(encoded.len() as u32).to_le_bytes()); + out.extend_from_slice(&encoded); + } + out + } + + /// Decode a receipts list produced by `encode_list`. + pub fn decode_list(encoded: &[u8]) -> Option> { + let mut cursor = 0usize; + let mut read = |len: usize| -> Option<&[u8]> { + if cursor + len > encoded.len() { + return None; + } + let slice = &encoded[cursor..cursor + len]; + cursor += len; + Some(slice) + }; + let count = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let mut receipts = Vec::with_capacity(count); + for _ in 0..count { + let len = + u32::from_le_bytes(read(4)?.try_into().ok()?) as usize; + let slice = read(len)?; + let (receipt, consumed) = TransactionReceipt::decode(slice)?; + if consumed != len { + return None; + } + receipts.push(receipt); + } + Some(receipts) + } } impl fmt::Display for TransactionReceipt { From 7234893835799253bd9874bb7af4f5edfd640866 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Tue, 30 Dec 2025 19:46:22 +0200 Subject: [PATCH 49/70] Restrict storage syscalls to caller address --- crates/kernel/src/syscall/storage.rs | 28 +++++++++++++++++++++++++++- crates/kernel/src/task/mod.rs | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index 887b250..931f07a 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -6,9 +6,10 @@ use core::cmp; use program::{log, logf}; use types::{Address, ADDRESS_LEN, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; -use crate::global::{CURRENT_TASK, STATE, TASKS}; +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; use crate::memory::page_allocator as mmu; use crate::syscall::alloc::sys_alloc; +use crate::task::TO_PTR_ADDR; use state::State; pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { @@ -35,6 +36,10 @@ pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { } addr_buf.copy_from_slice(&address_bytes); let address = Address(addr_buf); + if !caller_address_matches(root_ppn, &address) { + log!("sys_storage_get: address mismatch with caller"); + return 0; + } let domain_bytes = match read_user_bytes(root_ppn, domain_ptr, domain_len) { Some(bytes) => bytes, @@ -122,6 +127,10 @@ pub(crate) fn sys_storage_set(args: [u32; 6]) -> u32 { let mut addr_buf = [0u8; ADDRESS_LEN]; addr_buf.copy_from_slice(&address_bytes); let address = Address(addr_buf); + if !caller_address_matches(root_ppn, &address) { + log!("sys_storage_set: address mismatch with caller"); + return 0; + } let domain_bytes = match read_user_bytes(root_ppn, domain_ptr, domain_len) { Some(bytes) => bytes, @@ -200,6 +209,23 @@ pub(crate) fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option bool { + let current = unsafe { *CURRENT_TASK.get_mut() }; + if current == KERNEL_TASK_SLOT { + return true; + } + let caller_bytes = match read_user_bytes(root_ppn, TO_PTR_ADDR, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return false, + }; + if caller_bytes.len() != ADDRESS_LEN { + return false; + } + let mut caller_buf = [0u8; ADDRESS_LEN]; + caller_buf.copy_from_slice(&caller_bytes); + Address(caller_buf) == *address +} + fn hex_encode(bytes: &[u8]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut out = Vec::with_capacity(bytes.len().saturating_mul(2)); diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 5ba3e92..2f2c608 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -99,7 +99,7 @@ const TRAMPOLINE_CODE: [u32; 2] = [ 0x1020_0073, // sret ]; -const TO_PTR_ADDR: u32 = 0x120; +pub(crate) const TO_PTR_ADDR: u32 = 0x120; const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; From cc820bc44e6c5613b9aee5346bf7e500b4d3b65a Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 31 Dec 2025 12:31:18 +0200 Subject: [PATCH 50/70] kernel: add syscall program calls and shared program loader Create a shared program image loader to centralize account/code validation and entry offset detection. Implement syscall call-program flow with task setup, caller trapframe save, and result handoff back to user callers. Add result copying via caller heap allocation and adjust trap return behavior. Rework trampoline setup to share a single kernel trampoline page mapped into user roots and avoid reliance on current root. Add logging/cleanup tweaks and document kernel_run_task_inner. --- crates/kernel/src/bundle/program_call.rs | 86 ++++----------------- crates/kernel/src/lib.rs | 1 + crates/kernel/src/syscall/alloc.rs | 57 ++++++++------ crates/kernel/src/syscall/call_program.rs | 94 +++++++++++++++++++++++ crates/kernel/src/syscall/mod.rs | 18 +++-- crates/kernel/src/syscall/storage.rs | 2 +- crates/kernel/src/task/prep.rs | 36 ++++++--- crates/kernel/src/task/run.rs | 1 + crates/kernel/src/trap/mod.rs | 39 +++++++++- crates/kernel/src/user_program.rs | 81 +++++++++++++++++++ 10 files changed, 298 insertions(+), 117 deletions(-) create mode 100644 crates/kernel/src/syscall/call_program.rs create mode 100644 crates/kernel/src/user_program.rs diff --git a/crates/kernel/src/bundle/program_call.rs b/crates/kernel/src/bundle/program_call.rs index b53d3a1..2acea68 100644 --- a/crates/kernel/src/bundle/program_call.rs +++ b/crates/kernel/src/bundle/program_call.rs @@ -1,81 +1,29 @@ -use alloc::format; - -use kernel::{kernel_run_task, prep_program_task, Config, PROGRAM_WINDOW_BYTES}; -use kernel::global::{STATE, TASKS}; +use kernel::{kernel_run_task, prep_program_task, PROGRAM_WINDOW_BYTES}; +use kernel::global::TASKS; +use kernel::user_program::with_program_image; use program::{log, logf}; use program::parser::HexCodec; -use state::State; use types::transaction::Transaction; pub(crate) fn program_call(tx: &Transaction, resume: extern "C" fn() -> !) { - let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; - let account = match state.get_account(&tx.to) { - Some(acc) => acc, - None => { - logf!( - "%s", - display: format!("Program call failed: account {} does not exist", tx.to) - ); - return; - } - }; - - if !account.is_contract { - logf!( - "%s", - display: format!( - "Program call failed: target {} is not a contract (code_len={})", - tx.to, - account.code.len() - ) - ); - return; - } - - let first_nz = account - .code - .iter() - .position(|&b| b != 0) - .unwrap_or(account.code.len()); - let nz_count = account.code.iter().filter(|&&b| b != 0).count(); - logf!( - "%s", - display: format!( - "Program code stats: len={} first_nz={} nz_count={}", - account.code.len(), - first_nz, - nz_count - ) - ); - - let code_len = account.code.len(); - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; - if code_len > max { - panic!( - "❌ Program call rejected: code size ({}) exceeds limit ({})", - code_len, max - ); - } - let mut from_buf = [0u8; 40]; let mut to_buf = [0u8; 40]; let from_hex = HexCodec::encode(tx.from.as_ref(), &mut from_buf); let to_hex = HexCodec::encode(tx.to.as_ref(), &mut to_buf); - logf!( - "Program call: from=%s to=%s input_len=%d code_len=%d", - from_hex.as_ptr() as u32, - from_hex.len() as u32, - to_hex.as_ptr() as u32, - to_hex.len() as u32, - tx.data.len() as u32, - code_len as u32 - ); - - let entry_off = first_nz as u32; + let task = with_program_image(&tx.to, |image| { + logf!( + "Program call: from=%s to=%s input_len=%d code_len=%d", + from_hex.as_ptr() as u32, + from_hex.len() as u32, + to_hex.as_ptr() as u32, + to_hex.len() as u32, + tx.data.len() as u32, + image.code.len() as u32 + ); + prep_program_task(&tx.to, &tx.from, image.code, &tx.data, image.entry_off) + }); - if let Some(task) = - prep_program_task(&tx.to, &tx.from, &account.code, &tx.data, entry_off) - { + if let Some(task) = task { logf!( "Program task created: root=0x%x asid=%d window_size=%d", task.addr_space.root_ppn, @@ -99,6 +47,6 @@ pub(crate) fn program_call(tx: &Transaction, resume: extern "C" fn() -> !) { ); } } else { - log!("Program call skipped: no memory manager installed"); + panic!("program_call: no memory manager installed; cannot create program task"); } } diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index abf1e04..d9777b2 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -18,6 +18,7 @@ pub use task::{ pub mod memory; pub mod trap; pub mod syscall; +pub mod user_program; #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs index 67a0edd..4805466 100644 --- a/crates/kernel/src/syscall/alloc.rs +++ b/crates/kernel/src/syscall/alloc.rs @@ -1,47 +1,31 @@ use program::{log, logf}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; -pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { - let size = args[0]; - let align = args[1]; +use crate::Task; +pub(crate) fn alloc_in_task(task: &mut Task, size: u32, align: u32) -> Option { if size == 0 { log!("sys_alloc: invalid size 0"); - return 0; + return None; } if align == 0 || (align & (align - 1)) != 0 { logf!("sys_alloc: invalid alignment %d", align); - return 0; + return None; } - let current = unsafe { *CURRENT_TASK.get_mut() }; - // Kernel task should never call sys_alloc. - if current == KERNEL_TASK_SLOT { - panic!("sys_alloc: kernel task cannot allocate memory"); - } - - let tasks = unsafe { TASKS.get_mut() }; - let task = match tasks.get_mut(current) { - Some(task) => task, - None => { - logf!("sys_alloc: no current task for slot %d", current as u32); - return 0; - } - }; - let mask = align - 1; let start = match task.heap_ptr.checked_add(mask) { Some(addr) => addr & !mask, None => { log!("sys_alloc: heap ptr overflow"); - return 0; + return None; } }; let end = match start.checked_add(size) { Some(end) => end, None => { log!("sys_alloc: size overflow"); - return 0; + return None; } }; @@ -55,10 +39,35 @@ pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { window_base, window_limit ); - return 0; + return None; } task.heap_ptr = end; - start + Some(start) +} + +pub(crate) fn sys_alloc(args: [u32; 6]) -> u32 { + let size = args[0]; + let align = args[1]; + + let current = unsafe { *CURRENT_TASK.get_mut() }; + // Kernel task should never call sys_alloc. + if current == KERNEL_TASK_SLOT { + panic!("sys_alloc: kernel task cannot allocate memory"); + } + + let tasks = unsafe { TASKS.get_mut() }; + let task = match tasks.get_mut(current) { + Some(task) => task, + None => { + logf!("sys_alloc: no current task for slot %d", current as u32); + return 0; + } + }; + + match alloc_in_task(task, size, align) { + Some(addr) => addr, + None => 0, + } } pub(crate) fn sys_dealloc(_args: [u32; 6]) -> u32 { diff --git a/crates/kernel/src/syscall/call_program.rs b/crates/kernel/src/syscall/call_program.rs new file mode 100644 index 0000000..7f020f8 --- /dev/null +++ b/crates/kernel/src/syscall/call_program.rs @@ -0,0 +1,94 @@ +use program::logf; +use types::{Address, ADDRESS_LEN}; + +use crate::global::{CURRENT_TASK, TASKS}; +use crate::syscall::storage::{caller_address_matches, current_root_ppn, read_user_bytes}; +use crate::syscall::SyscallContext; +use crate::task::prep_program_task; +use crate::user_program::with_program_image; +use crate::Config; + +const REG_COUNT: usize = 32; +const REG_PC: usize = 32; + +pub(crate) fn sys_call_program(args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32 { + let to_ptr = args[0]; + let from_ptr = args[1]; + let input_ptr = args[2]; + let input_len = args[3] as usize; + + if input_len > Config::MAX_INPUT_LEN { + logf!("sys_call_program: input too large"); + return 0; + } + + let root_ppn = match current_root_ppn() { + Some(root) => root, + None => return 0, + }; + + let to_bytes = match read_user_bytes(root_ppn, to_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + let from_bytes = match read_user_bytes(root_ppn, from_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + let input = match read_user_bytes(root_ppn, input_ptr, input_len) { + Some(bytes) => bytes, + None => return 0, + }; + + if to_bytes.len() != ADDRESS_LEN || from_bytes.len() != ADDRESS_LEN { + logf!("sys_call_program: invalid address length"); + return 0; + } + + let mut to_buf = [0u8; ADDRESS_LEN]; + let mut from_buf = [0u8; ADDRESS_LEN]; + to_buf.copy_from_slice(&to_bytes); + from_buf.copy_from_slice(&from_bytes); + let to = Address(to_buf); + let from = Address(from_buf); + + if !caller_address_matches(root_ppn, &from) { + logf!("sys_call_program: caller address mismatch"); + return 0; + } + + let task = match with_program_image(&to, |image| { + prep_program_task(&to, &from, image.code, &input, image.entry_off) + }) { + Some(task) => task, + None => return 0, + }; + + let task_idx = unsafe { + let tasks = TASKS.get_mut(); + if tasks.push(task).is_err() { + logf!("sys_call_program: task list full"); + return 0; + } + tasks.len().saturating_sub(1) + }; + + let caller_idx = unsafe { *CURRENT_TASK.get_mut() }; + unsafe { + let tasks = TASKS.get_mut(); + let caller_task = match tasks.get_mut(caller_idx) { + Some(task) => task, + None => { + logf!("sys_call_program: missing caller task %d", caller_idx as u32); + return 0; + } + }; + for (idx, value) in ctx.regs.iter().take(REG_COUNT).enumerate() { + caller_task.tf.regs[idx] = *value; + } + caller_task.tf.pc = ctx.regs[REG_PC].wrapping_add(4); + } + + crate::run_task(task_idx); + 0 +} diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 6d37b2a..8b80cee 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -4,11 +4,13 @@ use program::{log, logf}; pub mod alloc; +pub mod call_program; pub mod fire_event; pub mod panic; pub mod storage; use alloc::{sys_alloc, sys_dealloc}; +use call_program::sys_call_program; use fire_event::sys_fire_event; use panic::sys_panic; use storage::{sys_storage_get, sys_storage_set}; @@ -32,13 +34,18 @@ pub enum CallerMode { Supervisor, } -pub fn dispatch_syscall(call_id: u32, args: [u32; 6], caller_mode: CallerMode) -> u32 { +pub struct SyscallContext<'a> { + pub regs: &'a mut [u32], + pub caller_mode: CallerMode, +} + +pub fn dispatch_syscall(call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32 { match call_id { SYSCALL_STORAGE_GET => sys_storage_get(args), SYSCALL_STORAGE_SET => sys_storage_set(args), SYSCALL_PANIC => sys_panic(args), - SYSCALL_LOG => sys_log(args, caller_mode), - SYSCALL_CALL_PROGRAM => sys_call_program(args), + SYSCALL_LOG => sys_log(args, ctx.caller_mode), + SYSCALL_CALL_PROGRAM => sys_call_program(args, ctx), SYSCALL_FIRE_EVENT => sys_fire_event(args), SYSCALL_ALLOC => sys_alloc(args), SYSCALL_DEALLOC => sys_dealloc(args), @@ -61,11 +68,6 @@ fn sys_log(_args: [u32; 6], caller_mode: CallerMode) -> u32 { 0 } -fn sys_call_program(_args: [u32; 6]) -> u32 { - log!("sys_call_program: need implementation"); - 0 -} - fn sys_transfer(_args: [u32; 6]) -> u32 { log!("sys_transfer: need implementation"); diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index 931f07a..72f7035 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -209,7 +209,7 @@ pub(crate) fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option bool { +pub(crate) fn caller_address_matches(root_ppn: u32, address: &Address) -> bool { let current = unsafe { *CURRENT_TASK.get_mut() }; if current == KERNEL_TASK_SLOT { return true; diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index ad37a1d..1529426 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,5 +1,5 @@ use crate::{AddressSpace, Config, Task}; -use crate::global::CURRENT_TASK; +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; use crate::memory::page_allocator as mmu; use program::{log, logf}; use types::address::Address; @@ -151,17 +151,13 @@ pub fn prep_program_task( // Install a small trampoline page mapped in both roots so we can switch // satp safely before jumping into the user program. let tramp_perms = mmu::PagePerms::user_rwx(); - if !mmu::map_user_range_for_root(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { - panic!("prep_program_task: failed to map trampoline page in user root"); - } - let _tramp_phys = match mmu::translate_user_va(root_ppn, TRAMPOLINE_VA) { - Some(p) => p as u32, - None => panic!("prep_program_task: trampoline VA not mapped"), + let kernel_root = unsafe { + TASKS + .get_mut() + .get(KERNEL_TASK_SLOT) + .map(|task| task.addr_space.root_ppn) + .unwrap_or_else(mmu::current_root) }; - if !mmu::mirror_user_range_into_kernel(root_ppn, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { - panic!("prep_program_task: failed to mirror trampoline into kernel root"); - } - let kernel_root = mmu::current_root(); let trap_entry = crate::trap::trap_entry as usize as u32; let trap_trampoline = build_trap_trampoline(kernel_root, trap_entry); // Stash both trampolines in a single shared page. @@ -175,9 +171,25 @@ pub fn prep_program_task( let base = TRAP_TRAMPOLINE_OFFSET + i * 4; tramp_bytes[base..base + 4].copy_from_slice(&word.to_le_bytes()); } - if !mmu::copy_into_user(root_ppn, TRAMPOLINE_VA, &tramp_bytes) { + if !mmu::map_user_range_for_root(kernel_root, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { + panic!("prep_program_task: failed to map trampoline page in kernel root"); + } + if !mmu::copy_into_user(kernel_root, TRAMPOLINE_VA, &tramp_bytes) { panic!("prep_program_task: failed to populate trampoline code"); } + let tramp_phys = match mmu::translate_user_va(kernel_root, TRAMPOLINE_VA) { + Some(p) => p as u32, + None => panic!("prep_program_task: trampoline VA not mapped in kernel root"), + }; + if !mmu::map_physical_range_for_root( + root_ppn, + TRAMPOLINE_VA, + tramp_phys, + PAGE_SIZE, + tramp_perms, + ) { + panic!("prep_program_task: failed to map trampoline page in user root"); + } let mut task = Task::new( AddressSpace::new( diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index 6bfb87e..509f5aa 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -141,6 +141,7 @@ pub unsafe extern "C" fn kernel_run_task(task_idx: usize) -> ! { ); } +/// Save the kernel trapframe into TASKS[0] and then jump into the requested task. extern "C" fn kernel_run_task_inner(saved: *const u32, task_idx: usize) -> ! { // Interpret the saved trap-frame as regs[0..31] + pc and copy it into TASKS[0]. let regs = unsafe { core::slice::from_raw_parts(saved, TRAP_FRAME_WORDS) }; diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index 5b9ff01..2235b5a 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -5,6 +5,7 @@ use types::result::{Result as VmResult, RESULT_DATA_SIZE}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, LAST_COMPLETED_TASK, TASKS}; use crate::memory::page_allocator as mmu; use crate::syscall; +use crate::syscall::alloc::alloc_in_task; use crate::syscall::storage::read_user_bytes; use crate::task::TRAMPOLINE_VA; use crate::{Config, Task}; @@ -137,7 +138,10 @@ pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { } else { syscall::CallerMode::User }; - let ret = syscall::dispatch_syscall(call_id, args, caller_mode); + let ret = { + let mut ctx = syscall::SyscallContext { regs, caller_mode }; + syscall::dispatch_syscall(call_id, args, &mut ctx) + }; regs[REG_A0] = ret; // a0 return value regs[REG_PC] = regs[REG_PC].wrapping_add(4); // Advance past ecall return_kind = 0; @@ -146,6 +150,7 @@ pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { SCAUSE_BREAKPOINT => { // Default to returning to the kernel task unless the current task has a caller. let mut caller_idx = KERNEL_TASK_SLOT; + let mut result_for_caller: Option = None; unsafe { let current = *CURRENT_TASK.get_mut(); let tasks = TASKS.get_mut(); @@ -155,6 +160,7 @@ pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { if let Some(result) = read_task_result(task) { task.last_result = Some(result); log_task_result(&result); + result_for_caller = Some(result); } else { log!("program result: failed to read result bytes"); } @@ -172,12 +178,23 @@ pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { } } // Restore the caller task's trapframe and address-space root. - if let Some(caller_task) = tasks.get(caller_idx) { + if let Some(caller_task) = tasks.get_mut(caller_idx) { + if caller_idx != KERNEL_TASK_SLOT { + let result_ptr = match result_for_caller { + Some(result) => write_result_to_caller(caller_task, &result).unwrap_or(0), + None => 0, + }; + caller_task.tf.regs[REG_A0] = result_ptr; + } for (idx, value) in caller_task.tf.regs.iter().take(REG_COUNT).enumerate() { regs[idx] = *value; } // Resume at the caller's return address. - regs[REG_PC] = caller_task.tf.regs[REG_RA]; + regs[REG_PC] = if caller_idx == KERNEL_TASK_SLOT { + caller_task.tf.regs[REG_RA] + } else { + caller_task.tf.pc + }; mmu::set_current_root(caller_task.addr_space.root_ppn); return_sp = caller_task.tf.regs[REG_SP]; logf!( @@ -268,6 +285,22 @@ fn log_task_result(result: &VmResult) { } } +fn write_result_to_caller(caller_task: &mut Task, result: &VmResult) -> Option { + let addr = alloc_in_task(caller_task, Config::MAX_RESULT_SIZE as u32, 4)?; + let mut buf = [0u8; Config::MAX_RESULT_SIZE]; + buf[0] = result.success as u8; + buf[1..5].copy_from_slice(&result.error_code.to_le_bytes()); + buf[5..9].copy_from_slice(&result.data_len.to_le_bytes()); + let data_len = (result.data_len as usize).min(RESULT_DATA_SIZE); + if data_len > 0 { + buf[9..9 + data_len].copy_from_slice(&result.data[..data_len]); + } + if !mmu::copy_into_user(caller_task.addr_space.root_ppn, addr, &buf) { + return None; + } + Some(addr) +} + #[inline(always)] fn read_stval() -> usize { let value: usize; diff --git a/crates/kernel/src/user_program.rs b/crates/kernel/src/user_program.rs new file mode 100644 index 0000000..e5c515d --- /dev/null +++ b/crates/kernel/src/user_program.rs @@ -0,0 +1,81 @@ +extern crate alloc; + +use alloc::format; +use program::logf; +use state::State; +use types::address::Address; + +use crate::global::STATE; +use crate::Config; + +pub struct ProgramImage<'a> { + pub code: &'a [u8], + pub entry_off: u32, +} + +// Load a program image from STATE, validate it, and pass a borrowed view to a caller. +// This centralizes code lookup, contract checks, and entry offset calculation. +pub fn with_program_image( + to: &Address, + f: impl FnOnce(ProgramImage<'_>) -> Option, +) -> Option { + // Fetch the account from state (or log and bail if it is missing). + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let account = match state.get_account(to) { + Some(acc) => acc, + None => { + logf!( + "%s", + display: format!("Program call failed: account {} does not exist", to) + ); + return None; + } + }; + + // Ensure the target is a contract; non-contract accounts cannot be executed. + if !account.is_contract { + logf!( + "%s", + display: format!( + "Program call failed: target {} is not a contract (code_len={})", + to, + account.code.len() + ) + ); + return None; + } + + // Find the first non-zero byte to infer the entry offset and log code stats. + let first_nz = account + .code + .iter() + .position(|&b| b != 0) + .unwrap_or(account.code.len()); + let nz_count = account.code.iter().filter(|&&b| b != 0).count(); + logf!( + "%s", + display: format!( + "Program code stats: len={} first_nz={} nz_count={}", + account.code.len(), + first_nz, + nz_count + ) + ); + + // Enforce the code size limit to prevent oversized binaries. + let code_len = account.code.len(); + let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + if code_len > max { + panic!( + "❌ Program call rejected: code size ({}) exceeds limit ({})", + code_len, max + ); + } + + // Provide the borrowed code slice and entry offset to the caller. + let entry_off = first_nz as u32; + f(ProgramImage { + code: &account.code, + entry_off, + }) +} From 9c58c9292b8241cd9056fdfdfc2a68d12545c368 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 31 Dec 2025 12:44:46 +0200 Subject: [PATCH 51/70] Remove avm crate and host interface - drop avm from the workspace and delete the crate sources - remove the VM host interface abstraction and its plumbing - update syscall wiring/tests and bootloader initialization - restore API trait import for bootloader memory helpers --- Cargo.lock | 13 - Cargo.toml | 1 - crates/avm/Cargo.toml | 13 - crates/avm/README.md | 81 --- crates/avm/src/avm.rs | 517 ------------------- crates/avm/src/execution_context.rs | 107 ---- crates/avm/src/global.rs | 12 - crates/avm/src/host_interface.rs | 114 ---- crates/avm/src/lib.rs | 13 - crates/avm/src/memory/memory_page.rs | 363 ------------- crates/avm/src/memory/memory_page_manager.rs | 90 ---- crates/avm/src/memory/mod.rs | 5 - crates/avm/src/metering.rs | 266 ---------- crates/avm/src/receipt.rs | 228 -------- crates/avm/src/router.rs | 21 - crates/avm/src/transaction.rs | 3 - crates/avm/tests/allocator_test.rs | 147 ------ crates/avm/tests/memory_page_offset.rs | 84 --- crates/avm/tests/spec_runner.rs | 283 ---------- crates/avm/tests/test_syscall_handler.rs | 106 ---- crates/bootloader/src/bootloader.rs | 4 - crates/bootloader/src/syscalls.rs | 65 +-- crates/bootloader/tests/allocator_test.rs | 12 +- crates/vm/src/cpu.rs | 8 +- crates/vm/src/exe.rs | 3 - crates/vm/src/host_interface.rs | 31 -- crates/vm/src/lib.rs | 1 - crates/vm/src/sys_call.rs | 2 - crates/vm/src/vm.rs | 21 +- 29 files changed, 31 insertions(+), 2583 deletions(-) delete mode 100644 crates/avm/Cargo.toml delete mode 100644 crates/avm/README.md delete mode 100644 crates/avm/src/avm.rs delete mode 100644 crates/avm/src/execution_context.rs delete mode 100644 crates/avm/src/global.rs delete mode 100644 crates/avm/src/host_interface.rs delete mode 100644 crates/avm/src/lib.rs delete mode 100644 crates/avm/src/memory/memory_page.rs delete mode 100644 crates/avm/src/memory/memory_page_manager.rs delete mode 100644 crates/avm/src/memory/mod.rs delete mode 100644 crates/avm/src/metering.rs delete mode 100644 crates/avm/src/receipt.rs delete mode 100644 crates/avm/src/router.rs delete mode 100644 crates/avm/src/transaction.rs delete mode 100644 crates/avm/tests/allocator_test.rs delete mode 100644 crates/avm/tests/memory_page_offset.rs delete mode 100644 crates/avm/tests/spec_runner.rs delete mode 100644 crates/avm/tests/test_syscall_handler.rs delete mode 100644 crates/vm/src/host_interface.rs diff --git a/Cargo.lock b/Cargo.lock index 843775e..5b20b64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,19 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "avm" -version = "0.1.0" -dependencies = [ - "bootloader", - "compiler", - "hex", - "state", - "storage", - "types", - "vm", -] - [[package]] name = "base16ct" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 19d12ab..3f21bc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,5 @@ [workspace] members = [ - "crates/avm", "crates/compiler", "crates/examples", "crates/bootloader", diff --git a/crates/avm/Cargo.toml b/crates/avm/Cargo.toml deleted file mode 100644 index b939a92..0000000 --- a/crates/avm/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "avm" -version = "0.1.0" -edition = "2024" - -[dependencies] -hex = { version = "0.4", default-features = false } -compiler = { path = "../compiler" } # adjust path as needed -state = { path = "../state" } # adjust path as needed -storage = { path = "../storage" } # adjust path as needed -vm = { path = "../vm" } # adjust path as needed -types = { path = "../types" } # adjust path as needed -bootloader = { path = "../bootloader" } diff --git a/crates/avm/README.md b/crates/avm/README.md deleted file mode 100644 index 589af92..0000000 --- a/crates/avm/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# AVM - Alon's Virtual Machine - -AVM is a custom virtual machine designed to execute Rust-compiled contracts in a sandboxed, stack-based environment. It features an isolated execution model where each contract call is executed within its own independent VM context. - ---- - -## 🧠 Core Concepts - -### Execution Stack - -AVM maintains an **execution stack** that tracks active `ExecutionContext` frames. Each time a contract calls another contract, a **new context** is pushed onto the stack. - -- The **top of the stack** is the currently executing context. -- When a call finishes, the context is popped and control returns to the caller. -- This model ensures **synchronous execution** and supports **reentrancy** and **deep contract composition**. - -### ExecutionContext - -An `ExecutionContext` is a complete, self-contained VM instance with its own: - -- **Registers** (`[u64; 32]`) -- **Program Counter (`PC`)** -- **Memory Page** (linear memory, typically fixed-size per context) -- **Stack Pointer / Heap management** -- **Gas metering** (optional but recommended) -- **Access to the syscall interface** - -Each context behaves like a separate "machine" running in isolation. - ---- - -## 🔄 Contract-to-Contract Calls - -When Contract A calls Contract B: -1. A new `ExecutionContext` is initialized with: - - B's bytecode loaded - - Fresh memory and registers - - Arguments passed via memory or registers -2. The new context is **pushed** onto the execution stack. -3. Execution begins in the new context. -4. Upon return, the result is passed back to the caller, and the callee context is **popped**. - -This mechanism provides: -- Full isolation between contracts -- Clean memory separation (no shared heap or stack) -- Easy error handling (unwinding stack on `panic`) - ---- - -## 📦 Memory Model - -Each context is allocated its own **linear memory page**, which includes: - -- `.text` (program code, optional if interpreted) -- `.rodata` and statics (copied at load time) -- Stack (grows down from high memory) -- Heap (grows up from a defined base) - -All memory access is **local to the context**, preventing accidental overwrites between contracts. - ---- - -## 🚀 Features (Planned or In Progress) - -- [x] RISC-V instruction decoding (32-bit and compressed) -- [x] Memory-mapped syscall interface -- [x] Per-context execution model -- [x] Gas accounting and metering -- [ ] Persistent storage via key-based syscalls -- [ ] Support for `vm_panic` and return codes -- [ ] Debug output and tracing - ---- - -## 🧪 Example Use Case - -```rust -#[contract] -fn contract_a() { - call_contract("contract_b", &[arg1, arg2]); -} diff --git a/crates/avm/src/avm.rs b/crates/avm/src/avm.rs deleted file mode 100644 index ec82e44..0000000 --- a/crates/avm/src/avm.rs +++ /dev/null @@ -1,517 +0,0 @@ -use crate::execution_context::{ContextStack, ExecutionContext}; -use crate::global::Config; -use crate::host_interface::HostShim; -use crate::memory::MemoryPageManager; -use crate::metering::{GasMeter, SharedGasMeter}; -use crate::receipt::TransactionReceipt; -use crate::transaction::{Transaction, TransactionType}; -use core::cell::RefCell; -use core::fmt::Write; -use state::{Account, State}; -use std::rc::Rc; -use std::{ - panic::{AssertUnwindSafe, catch_unwind}, - usize, -}; -use storage::Storage; -use types::address::Address; -use types::result::Result; -use vm::registers::Register; -use vm::vm::VM; -use bootloader::DefaultSyscallHandler; - -/// Application Virtual Machine (AVM) - the main orchestrator for smart contract execution. -/// -/// EDUCATIONAL PURPOSE: This struct represents a complete blockchain virtual machine -/// that can execute smart contracts. It's similar to Ethereum's EVM or other blockchain VMs. -/// -/// AVM ARCHITECTURE OVERVIEW: -/// - Context Stack: Manages nested contract calls (like a call stack in programming) -/// - Memory Manager: Allocates and manages memory pages for contract execution -/// - Storage: Global persistent storage shared across all contracts -/// - State: Manages accounts, balances, and contract code -/// -/// BLOCKCHAIN CONCEPTS: -/// - Each contract has its own account with code and storage -/// - Transactions can create accounts or call existing contracts -/// - Contracts can call other contracts (nested execution) -/// - All state changes are atomic (all succeed or all fail) -/// -/// REAL-WORLD BLOCKCHAIN COMPARISON: -/// This AVM is inspired by Ethereum's EVM but simplified for educational purposes: -/// - Ethereum has more complex gas accounting and pricing -/// - Real blockchains have more sophisticated memory management -/// - Production VMs include additional security features like reentrancy protection -/// - Gas limits and execution timeouts prevent infinite loops -/// -/// VIRTUAL MACHINE LAYERS: -/// The AVM operates at multiple abstraction levels: -/// 1. Transaction Layer: Processes blockchain transactions -/// 2. Contract Layer: Executes smart contract bytecode -/// 3. Memory Layer: Manages contract memory allocation -/// 4. Storage Layer: Provides persistent data storage -/// 5. State Layer: Maintains global blockchain state -/// -/// SECURITY CONSIDERATIONS: -/// - Panic handling prevents one bad contract from crashing the entire system -/// - Memory isolation between contracts prevents interference -/// - Input validation prevents resource exhaustion attacks -/// - Context tracking prevents unauthorized cross-contract access -pub struct AVM { - /// Stack of execution contexts for nested contract calls. - /// - /// EDUCATIONAL: This implements a call stack similar to how functions - /// call other functions in programming. Each context tracks who called - /// whom and with what data. This is crucial for debugging and gas accounting. - pub context_stack: ContextStack, - - /// Manages allocation of memory pages for contract execution. - /// - /// EDUCATIONAL: Each contract gets its own memory page to prevent - /// interference between contracts. This is like process isolation in - /// operating systems - one process can't access another's memory. - pub memory_manager: MemoryPageManager, - - /// Global state of the AVM including all accounts and their data. - /// - /// EDUCATIONAL: This represents the entire blockchain state - all - /// accounts, their balances, code, and storage. Every transaction - /// can potentially modify this state. - pub state: State, - - /// Shared gas meter that all contract calls within a transaction use. - /// Nested calls borrow the same counter so they cannot mint gas by - /// re-entering other contracts. - pub gas_meter: Rc>, - - pub verbose: bool, // Enable verbose logging for debugging - - /// Optional writer for verbose output. If None, outputs to console. - pub verbose_writer: Option>>, -} - -impl std::fmt::Debug for AVM { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AVM") - .field("context_stack", &self.context_stack) - .field("memory_manager", &self.memory_manager) - .field("state", &self.state) - .field("gas_used", &self.gas_meter.borrow().used()) - .field("verbose", &self.verbose) - .field("verbose_writer", &self.verbose_writer.as_ref().map(|_| "Some()")) - .finish() - } -} - -impl AVM { - pub fn set_verbosity(&mut self, value: bool) { - self.verbose = value; - } - - /// Sets the output writer for verbose logging. - /// If set, verbose output will be written to this writer instead of console. - pub fn set_verbose_writer(&mut self, writer: Rc>) { - self.verbose_writer = Some(writer); - } - - /// Total gas consumed so far in the current AVM instance. - pub fn gas_used(&self) -> u64 { - self.gas_meter.borrow().used() - } - - /// Helper method to log output to either console or the configured writer - /// Only logs if verbose is true and self.verbose is enabled - fn log(&self, message: &str, verbose: bool) { - // Only log if this is not a verbose message, or if verbose logging is enabled - if verbose && !self.verbose { - return; - } - - match &self.verbose_writer { - Some(writer) => { - // Write to the provided writer (add newline manually) - let _ = write!(writer.borrow_mut(), "{}\n", message); - } - None => { - // Output to console - println!("{}", message); - } - } - } - - /// Creates a new Application Virtual Machine with specified memory constraints. - /// - /// EDUCATIONAL PURPOSE: This demonstrates VM initialization with resource limits. - /// In blockchain systems, resource limits are crucial to prevent denial-of-service - /// attacks and ensure predictable execution costs. - /// - /// RESOURCE MANAGEMENT: - /// - max_pages: Maximum number of memory pages that can be allocated - /// - page_size: Size of each memory page in bytes - /// - /// INITIALIZATION: All components start in a clean state, ready to - /// process transactions and execute contracts. - pub fn new(max_pages: usize, page_size: usize, state: State) -> Self { - Self { - context_stack: ContextStack::new(), - memory_manager: MemoryPageManager::new(max_pages, page_size), - state, - gas_meter: Rc::new(RefCell::new(GasMeter::new())), - verbose: false, // Default to no verbose logging - verbose_writer: None, // Default to console output - } - } - - /// Executes a transaction, which can be a transfer, account creation, or contract call. - /// - /// EDUCATIONAL PURPOSE: This is the main entry point for processing blockchain - /// transactions. Each transaction type has different semantics and security considerations. - /// - /// TRANSACTION TYPES: - /// - Transfer: Move value between accounts (not implemented in this VM) - /// - CreateAccount: Deploy a new smart contract - /// - ProgramCall: Execute an existing smart contract - /// - /// TRANSACTION PROCESSING FLOW: - /// 1. Validate transaction format and parameters - /// 2. Check account existence and permissions - /// 3. Execute the appropriate operation based on transaction type - /// 4. Update global state with the results - /// 5. Return success/failure status - /// - /// ATOMICITY: All state changes within a transaction are atomic - either - /// all succeed or all fail. This ensures data consistency even if the - /// system crashes during transaction processing. - /// - /// ERROR HANDLING: Uses catch_unwind to prevent panics from crashing the entire - /// system. This is crucial in blockchain systems where one bad transaction - /// shouldn't affect others. - /// - /// GAS ACCOUNTING: A shared gas meter (EVM-inspired schedule) is shared - /// across nested calls to prevent gas creation. - /// - /// RETURN VALUE: Returns a Result indicating success/failure and any error codes - pub fn run_tx(&mut self, tx: Transaction) -> TransactionReceipt { - match tx.tx_type { - TransactionType::Transfer => { - // EDUCATIONAL: Value transfer between accounts - let ok = self.apply_transfer(tx.from, tx.to, tx.value); - return TransactionReceipt::new(tx, Result::new(ok, if ok { 0 } else { 1 })); - } - - TransactionType::CreateAccount => { - let result = catch_unwind(AssertUnwindSafe(|| { - self.create_account(tx.from, tx.to, tx.data.clone()); - })); - - // EDUCATIONAL: Handle deployment failures gracefully - if let Err(_e) = result { - return TransactionReceipt::new(tx, Result::new(false, 1)); - } else { - return TransactionReceipt::new(tx, Result::new(true, 0)); - } - } - - TransactionType::ProgramCall => { - // EDUCATIONAL: Execute an existing smart contract - // First verify the destination is actually a contract - assert!( - self.state.is_contract(tx.to), - "destination address is not a contract" - ); - - // EDUCATIONAL: Call the contract and extract the result - let (result_ptr, context_index) = - self.call_contract(tx.from, tx.to, tx.data.clone()); - - // verify context stack is empty - if !self.context_stack.is_empty() { - if self.context_stack.iter().any(|ctx| !ctx.exe_done) { - panic!("context stack has unfinished contexts after tx execution"); - } - } - - // extract result - let res = self.extract_result(result_ptr, context_index); - TransactionReceipt::new(tx, res) - // Include events from this context and any nested calls. - .set_events(self.context_stack.collect_events_from(context_index)) - } - } - } - - /// Moves native tokens between two accounts. Returns true on success. - pub fn apply_transfer(&mut self, from: Address, to: Address, amount: u64) -> bool { - let amount = amount as u128; - let from_account = self.state.get_account_mut(&from); - if from_account.balance < amount { - return false; - } - from_account.balance -= amount; - - let to_account = self.state.get_account_mut(&to); - to_account.balance = to_account.balance.saturating_add(amount); - true - } - - /// Extracts the result of a contract execution from memory. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how contract results are communicated - /// back to the caller. The contract writes its result to a specific memory location, - /// and this function reads it. - /// - /// RESULT FORMAT: The result is stored as a 261-byte structure: - /// - 1 byte: success flag (0 = false, non-zero = true) - /// - 4 bytes: error code (u32) - /// - 4 bytes: data length (u32) - /// - 256 bytes: data array - /// - /// MEMORY SAFETY: Validates that the result pointer is within bounds - /// to prevent reading invalid memory. - fn extract_result(&self, _result_ptr: u32, context_index: usize) -> Result { - // EDUCATIONAL: Get the memory page where the result was stored - let ee = self - .context_stack - .get(context_index) - .expect("missing execution context"); - let vm = ee.vm.borrow(); - let page = vm.memory.as_ref(); - - // EDUCATIONAL: Use the memory page's offset calculation to get the correct memory location - let start = page.offset(Config::RESULT_ADDR as usize); // Use memory page offset - - // EDUCATIONAL: Validate memory bounds to prevent out-of-bounds access - if start + Config::MAX_RESULT_SIZE > page.size() { - panic!("Result struct out of bounds at 0x{:08x}", start); - } - - // EDUCATIONAL: Extract the result fields from memory using the correct offset - let mem = page.mem(); - - let success = mem[start] != 0; - let error_code = u32::from_le_bytes(mem[start+1..start + 5].try_into().unwrap()); - let data_len = u32::from_le_bytes(mem[start+5..start + 9].try_into().unwrap()); - - // EDUCATIONAL: Extract the data array - let mut data = [0u8; 256]; - data.copy_from_slice(&mem[start+9..start + 265]); - - return Result { success, error_code, data_len, data }; - } - - /// Creates a new account (smart contract) with the provided code. - /// - /// EDUCATIONAL PURPOSE: This demonstrates smart contract deployment. - /// In blockchain systems, deploying a contract creates a new account - /// that can hold code and persistent storage. - /// - /// SECURITY CHECKS: - /// - Ensures the target address isn't already in use - /// - Validates code size limits to prevent resource exhaustion - /// - /// ACCOUNT CREATION: Creates an Account struct with: - /// - code: The smart contract bytecode - /// - storage: Empty persistent storage - /// - balance: 0 (no initial funds) - /// - nonce: 0 (no transactions yet) - /// - is_contract: true (marks this as a contract account) - pub fn create_account(&mut self, _from: Address, to: Address, data: Vec) { - // EDUCATIONAL: Deploy a new smart contract - // This creates a new account with the provided code - let is_contract = !data.is_empty(); - let code_size = data.len(); - - self.log(&format!( - "Tx creating account at address {}. Is contract: {}. Code size: {} bytes.", - to, - is_contract, - code_size - ), false); - - // EDUCATIONAL: Check that the target address is not already in use - // This prevents overwriting existing accounts - if self.state.accounts.contains_key(&to) { - panic!("account already exists"); - } - - // EDUCATIONAL: Validate code size limits - // This prevents resource exhaustion attacks - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; - if data.len() > max { - panic!( - "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", - data.len(), - max - ); - } - - // EDUCATIONAL: Create and insert new account with code - let account = Account { - code: data, // The smart contract bytecode - storage: Default::default(), // Empty persistent storage - balance: 0, // No initial balance - nonce: 0, // No transactions yet - is_contract: true, // Mark as contract account - }; - - self.state.accounts.insert(to, account); - } - - /// Handles calling a new contract, spinning up a fresh VM with its own memory page. - /// - /// EDUCATIONAL PURPOSE: This demonstrates smart contract execution. - /// Each contract call gets its own isolated VM instance to prevent - /// interference between contracts. - /// - /// EXECUTION PROCESS: - /// 1. Validate the target is a contract - /// 2. Allocate fresh memory and storage - /// 3. Set up the VM with contract code and parameters - /// 4. Execute the contract safely - /// 5. Extract and return the result - /// - /// ISOLATION: Each contract gets its own memory page and storage - /// to prevent one contract from affecting another. - /// - /// PARAMETER PASSING: Contract parameters are passed through CPU registers: - /// - a0: Contract address (who is being called) - /// - a1: Caller address (who is making the call) - /// - a2: Input data pointer - /// - a3: Input data length - /// - a4: Result pointer (where to write the result) - pub fn call_contract(&mut self, from: Address, to: Address, input_data: Vec) -> (u32, usize) { - self.log(&format!( - "Tx calling program at address {} with data 0x{}", - to, - hex::encode(&input_data) - ), false); - - // Charge the entry call once at the transaction root. Nested contract - // calls initiated via SYSCALL_CALL_PROGRAM are already metered through - // the VM's on_call hook. - if self.context_stack.is_empty() { - let _ = self.gas_meter.borrow_mut().charge_call(input_data.len()); - } - - // Save address for later use in termination log - let to_addr_str = to.to_string(); - - // SAFETY NOTE: - // This line creates a HostShim containing a raw pointer (*mut AVM) to self. - // Even though raw pointers don't participate in Rust's borrow checker, - // calling `HostShim::new(self)` still *temporarily borrows* `self` as `&mut AVM` - // during this line. If `self` is already mutably borrowed (e.g. for pushing to context_stack, - // accessing state, or memory_manager), this will cause a compile-time error due to overlapping mutable borrows. - // To avoid this, ensure all other mutable uses of `self` happen *before* or *after* this line. - let shim = HostShim::new(self); - - // EDUCATIONAL: Get mutable reference to the contract account - let account = self.state.get_account_mut(&to); - if !account.is_contract { - panic!("destination address {} is not a contract", to); - } - - // EDUCATIONAL: Allocate memory and clone storage for isolation - let memory_page = self.memory_manager.new_page(); - let storage = Rc::new(RefCell::new(Storage::with_map(account.storage.clone()))); - - // EDUCATIONAL: Create and configure child VM - // We use Box here to heap-allocate the HostShim and pass it as a trait object (Box). - // This is necessary because `VM` stores the host as `Box`, which: - // - Allows us to erase the concrete type (HostShim) at compile time - // - Removes the need for lifetimes like &'a mut dyn HostInterface - // - Enables recursive call_contract logic, since the Box owns the host and doesn't borrow `self` - // Without Box, we would need to track lifetimes manually and would hit borrow checker issues. - let mut vm: VM = VM::new( - memory_page, - storage.clone(), - Box::new(shim), - Box::new(DefaultSyscallHandler::with_writer(self.verbose_writer.clone())), - ); - let shared_meter = SharedGasMeter::new(Rc::clone(&self.gas_meter)); - vm.set_metering(Box::new(shared_meter)); - vm.set_code(0, Config::PROGRAM_START_ADDR, &account.code); - vm.cpu.verbose = self.verbose; - - // Set up logging writer for CPU to use AVM's logging mechanism - if let Some(writer) = &self.verbose_writer { - vm.cpu.set_verbose_writer(writer.clone()); - } - - // add new context execution - let context_index = self.context_stack.push(from, to, input_data, vm); - let context = self - .context_stack - .current_mut() - .expect("missing execution context"); - - // EDUCATIONAL: Set up function parameters in registers - // This follows the RISC-V calling convention - let _address_ptr = context - .vm - .borrow_mut() - .set_reg_to_data(Register::A0, to.0.as_ref()); // Contract address - let _pubkey_ptr = context - .vm - .borrow_mut() - .set_reg_to_data(Register::A1, from.0.as_ref()); // Caller address - - // EDUCATIONAL: Validate input size to prevent resource exhaustion - let input_len = context.input_data.len(); - if input_len > Config::MAX_INPUT_LEN { - panic!( - "Entrypoint: input length {} exceeds MAX_INPUT_LEN ({})", - input_len, - Config::MAX_INPUT_LEN - ); - } - - // EDUCATIONAL: Set up input data (no result pointer needed) - let _input_ptr = context - .vm - .borrow_mut() - .set_reg_to_data(Register::A2, &context.input_data); // Input data - context - .vm - .borrow_mut() - .set_reg_u32(Register::A3, input_len as u32); // Input length - - // EDUCATIONAL: Run the VM safely with panic handling - let result = catch_unwind(AssertUnwindSafe(|| { - context.vm.borrow_mut().raw_run(); - })); - - // EDUCATIONAL: Handle VM panics gracefully - if let Err(e) = result { - eprintln!("💥 VM panicked: {:?}", e); - panic!("VM panicked"); - } - - // EDUCATIONAL: Copy storage back into account - // This persists any changes the contract made to storage - let updated_map = storage.borrow().map.borrow().clone(); - account.storage = updated_map; - - // EDUCATIONAL: set context execution done - context.exe_done = true; - - // Log execution termination for binary comparison tracking (after all borrows are done) - self.log( - &format!("Execution terminated for address {}", to_addr_str), - false, - ); - - (Config::RESULT_ADDR, context_index) // Fixed result address - } - - /// Peek the current active execution context. - /// - /// EDUCATIONAL PURPOSE: This allows inspection of the current execution - /// context, which is useful for debugging and understanding the call stack. - /// - /// USAGE: Typically used by debugging tools or for implementing features - /// like gas accounting or call tracing. - pub fn current_context(&self) -> Option<&ExecutionContext> { - self.context_stack.current() - } -} diff --git a/crates/avm/src/execution_context.rs b/crates/avm/src/execution_context.rs deleted file mode 100644 index 2327b72..0000000 --- a/crates/avm/src/execution_context.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; -use types::address::Address; -use vm::vm::VM; - -/// Represents a single execution context during contract calls. -#[derive(Debug, Clone)] -pub struct ExecutionContext { - /// The address that initiated the current call. - pub from: Address, - - /// The address currently receiving the call. - pub to: Address, - - // Data passed to the contract call - pub input_data: Rc>, - - // Memory page - pub vm: Rc>, - - pub events: Vec>, - - // is exe_done marks context as executed - pub exe_done: bool, -} - -impl ExecutionContext { - pub fn new(from: Address, to: Address, input_data: Vec, vm: VM) -> Self { - Self { - from, - to, - input_data: Rc::new(input_data), - vm: Rc::new(RefCell::new(vm)), - events: Vec::new(), - exe_done: false, - } - } -} - -/// A call stack for nested execution contexts in the VM. -#[derive(Debug)] -pub struct ContextStack { - stack: Vec, -} - -impl ContextStack { - /// Create a new, empty context stack. - pub fn new() -> Self { - Self { stack: Vec::new() } - } - - /// Push a new context onto the stack (e.g., when a contract calls another). - /// returns index of the new execution context - pub fn push(&mut self, from: Address, to: Address, input_data: Vec, vm: VM) -> usize { - let index = self.stack.len(); - self.stack.push(ExecutionContext { - from, - to, - input_data: Rc::new(input_data), - vm: Rc::new(RefCell::new(vm)), - events: Vec::new(), - exe_done: false, - }); - index - } - - /// Pop the most recent context off the stack (e.g., when returning from a call). - pub fn pop(&mut self) -> Option { - self.stack.pop() - } - - /// Peek execution context index without modifying the stack. - pub fn get(&self, i: usize) -> Option<&ExecutionContext> { - self.stack.get(i) - } - - pub fn get_mut(&mut self, i: usize) -> Option<&mut ExecutionContext> { - self.stack.get_mut(i) - } - - /// Peek at the current execution context without modifying the stack. - pub fn current(&self) -> Option<&ExecutionContext> { - self.stack.last() - } - - pub fn current_mut(&mut self) -> Option<&mut ExecutionContext> { - self.stack.last_mut() - } - - pub fn iter(&self) -> impl Iterator { - self.stack.iter() - } - - pub fn is_empty(&self) -> bool { - self.stack.is_empty() - } - - /// Collect all events from a starting context index through the top of the stack. - pub fn collect_events_from(&self, start: usize) -> Vec> { - self.stack - .iter() - .enumerate() - .filter(|(idx, _)| *idx >= start) - .flat_map(|(_, ctx)| ctx.events.clone()) - .collect() - } -} diff --git a/crates/avm/src/global.rs b/crates/avm/src/global.rs deleted file mode 100644 index e39b61b..0000000 --- a/crates/avm/src/global.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub struct Config; - -impl Config { - pub const MAX_INPUT_LEN: usize = 1024; - pub const CODE_SIZE_LIMIT: usize = 0x30000; // 192KB headroom for non-compressed RV32IM binaries - pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; // 8KB for read-only data - pub const HEAP_START_ADDR: usize = Self::CODE_SIZE_LIMIT + Self::RO_DATA_SIZE_LIMIT + 0x100; - pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; - - pub const PROGRAM_START_ADDR: u32 = 0x400; - pub const RESULT_ADDR: u32 = 0x100; -} diff --git a/crates/avm/src/host_interface.rs b/crates/avm/src/host_interface.rs deleted file mode 100644 index 32f94f8..0000000 --- a/crates/avm/src/host_interface.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::avm::AVM; -use types::address::Address; -use vm::host_interface::HostInterface; - -// HostShim is a lightweight adapter that allows a VM to call back into the AVM. -// It implements the HostInterface trait and holds a raw pointer to the AVM. -// -// We use a raw pointer (*mut AVM) instead of &'a mut AVM to avoid borrow checker conflicts. -// This is necessary because AVM::call_contract creates new VMs, which also require access -// to the AVM via HostInterface. If we used &'a mut AVM, we’d get lifetime or multiple mutable -// borrow errors due to recursive calls. -// -// By using *mut AVM: -// - We avoid tracked mutable borrows -// - We preserve safety by ensuring the pointer is only dereferenced during the call -// - We allow recursive VM execution without violating Rust's ownership model -// -// This approach is safe in our case because: -// - Each VM invocation gets its own HostShim -// - The pointer never escapes its VM or outlives AVM -// - We do not access AVM concurrently or from multiple threads -#[derive(Debug)] -pub struct HostShim { - pub avm_ptr: *mut AVM, // raw pointer to the AVM -} - -impl HostShim { - pub fn new(avm: &mut AVM) -> Self { - HostShim { - avm_ptr: avm as *mut AVM, - } - } -} - -impl<'a> HostInterface for HostShim { - fn call_program(&mut self, from: [u8; 20], to: [u8; 20], input_data: Vec) -> (u32, usize) { - unsafe { - return (*self.avm_ptr).call_contract(Address(from), Address(to), input_data); - } - } - - fn fire_event(&mut self, event: Vec) { - unsafe { - // SAFETY: self.avm_ptr must point to a valid AVM that has access to the callee's memory - let avm = &mut *self.avm_ptr; - avm.context_stack - .current_mut() - .expect("must have current context") - .events - .push(event.clone()); - - let hex_string: String = event - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect::>() - .join(" "); - - println!("[sys_fire_event] Event bytes (hex): {}", hex_string); - } - } - - fn read_memory_page( - &mut self, - page_index: usize, - guest_ptr: u32, - len: usize, - ) -> Option> { - unsafe { - // SAFETY: self.avm_ptr must point to a valid AVM that has access to the callee's memory - let avm = &*self.avm_ptr; - - let ee = avm - .context_stack - .get(page_index) - .expect("missing execution context"); - let vm = ee.vm.borrow(); - let page_ref = vm.memory.as_ref(); - - // Assume the callee's memory manager is accessible here - let mem = page_ref.mem(); - - let start = guest_ptr as usize; - let end = start.checked_add(len)?; - - if end > mem.len() { - return None; // Out of bounds - } - - Some(mem[start..end].to_vec()) - } - } - - fn transfer(&mut self, to: [u8; 20], value: u64) -> bool { - unsafe { - let avm = &mut *self.avm_ptr; - let to_addr = Address(to); - - // Use the active execution context to determine the sender. - let ctx = match avm.context_stack.current() { - Some(c) => c, - None => return false, - }; - avm.apply_transfer(ctx.from, to_addr, value) - } - } - - fn balance(&mut self, addr: [u8; 20]) -> u128 { - unsafe { - let avm = &mut *self.avm_ptr; - let account = avm.state.get_account(&Address(addr)); - account.map(|a| a.balance).unwrap_or(0) - } - } -} diff --git a/crates/avm/src/lib.rs b/crates/avm/src/lib.rs deleted file mode 100644 index dd6c9b3..0000000 --- a/crates/avm/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -// external -pub extern crate hex; - -// exports -pub mod avm; -pub mod execution_context; -pub mod global; -pub mod host_interface; -pub mod memory; -pub mod metering; -pub mod receipt; -pub mod router; -pub mod transaction; diff --git a/crates/avm/src/memory/memory_page.rs b/crates/avm/src/memory/memory_page.rs deleted file mode 100644 index 318f6f1..0000000 --- a/crates/avm/src/memory/memory_page.rs +++ /dev/null @@ -1,363 +0,0 @@ -use std::cell::{Cell, Ref, RefCell}; -use std::convert::TryInto; -use std::rc::Rc; -use vm::memory::{memory, HEAP_PTR_OFFSET}; -use vm::metering::{MeterResult, Metering, MemoryAccessKind}; - -#[derive(Debug, Clone)] -pub struct MemoryPage { - mem: Rc>>, - pub next_heap: Cell, - pub base_address: usize, // base address for guest memory mapping -} - -impl MemoryPage { - pub fn new_with_base(memory_size: usize, base_address: usize) -> Self { - Self { - mem: Rc::new(RefCell::new(vec![0u8; memory_size])), - next_heap: Cell::new(0), - base_address, - } - } - - pub fn new(memory_size: usize) -> Self { - Self::new_with_base(memory_size, 0) - } - - pub fn mem(&self) -> Ref> { - self.mem.borrow() - } - - pub fn size(&self) -> usize { - let mem = self.mem(); - mem.len() - } - - pub fn offset(&self, addr: usize) -> usize { - addr.checked_sub(self.base_address) - .expect("Address below base_address") - } - - fn meter_access( - metering: &mut dyn Metering, - kind: MemoryAccessKind, - addr: usize, - bytes: usize, - ) -> bool { - matches!( - metering.on_memory_access(kind, addr, bytes), - MeterResult::Continue - ) - } - - pub fn store_u16( - &self, - addr: usize, - val: u16, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - if !Self::meter_access(metering, kind, addr, 2) { - return false; - } - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset + 2 > mem.len() { - panic!("store u16 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset..offset + 2].copy_from_slice(&val.to_le_bytes()); - true - } - - pub fn store_u32( - &self, - addr: usize, - val: u32, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - if !Self::meter_access(metering, kind, addr, 4) { - return false; - } - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset + 4 > mem.len() { - panic!("store u32 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); - true - } - - pub fn store_u8( - &self, - addr: usize, - val: u8, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - if !Self::meter_access(metering, kind, addr, 1) { - return false; - } - let offset = self.offset(addr); - let mut mem = self.mem.borrow_mut(); - if offset >= mem.len() { - panic!("store u8 out of bounds: addr = 0x{:08x}", addr); - } - mem[offset] = val; - true - } - - pub fn load_u32( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 4) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - if offset + 4 > mem.len() { - panic!("load u32 out of bounds: addr = 0x{:08x}", addr); - } - Some(u32::from_le_bytes( - mem[offset..offset + 4].try_into().unwrap(), - )) - } - - pub fn load_byte( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 1) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - Some(mem[offset]) - } - - pub fn load_halfword( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 2) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - Some(u16::from_le_bytes( - mem[offset..offset + 2].try_into().unwrap(), - )) - } - - pub fn load_word( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - if !Self::meter_access(metering, kind, addr, 4) { - return None; - } - let offset = self.offset(addr); - let mem = self.mem.borrow(); - Some(u32::from_le_bytes( - mem[offset..offset + 4].try_into().unwrap(), - )) - } - - pub fn mem_slice(&self, start: usize, end: usize) -> Option> { - let start_offset = self.offset(start); - let end_offset = self.offset(end); - let mem_ref = self.mem.borrow(); - if end_offset > mem_ref.len() || start_offset > end_offset { - return None; - } - Some(std::cell::Ref::map(mem_ref, move |v| &v[start_offset..end_offset])) - } - - pub fn write_code(&self, start_addr: usize, code: &[u8]) { - let start_offset = self.offset(start_addr); - let mut mem = self.mem.borrow_mut(); - let end = start_offset + code.len(); - mem[start_offset..end].copy_from_slice(code); - - // set heap pointer - self.next_heap - .set(start_offset as u32 + code.len() as u32 + HEAP_PTR_OFFSET); - } - - pub fn alloc_on_heap(&self, data: &[u8]) -> u32 { - let mut addr = self.next_heap.get(); - - // Align to 4 bytes (or 8 if you're storing u64s) - let align = 8; - addr = (addr + (align - 1)) & !(align - 1); - - let end = addr + data.len() as u32; - assert!( - end as usize <= self.size(), - "Out of memory: trying to allocate {} bytes, but only {} bytes available", - data.len(), - self.size() - addr as usize - ); - - self.mem.borrow_mut()[addr as usize..end as usize].copy_from_slice(data); - self.next_heap.set(end); - - addr - } - - pub fn stack_top(&self) -> u32 { - self.size() as u32 - } -} - -impl Default for MemoryPage { - fn default() -> Self { - MemoryPage::new(4096) - } -} - -impl memory for MemoryPage { - fn mem(&self) -> Ref> { - MemoryPage::mem(self) - } - - fn mem_slice(&self, start: usize, end: usize) -> Option> { - MemoryPage::mem_slice(self, start, end) - } - - fn store_u16( - &self, - addr: usize, - val: u16, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - MemoryPage::store_u16(self, addr, val, metering, kind) - } - - fn store_u32( - &self, - addr: usize, - val: u32, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - MemoryPage::store_u32(self, addr, val, metering, kind) - } - - fn store_u8( - &self, - addr: usize, - val: u8, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> bool { - MemoryPage::store_u8(self, addr, val, metering, kind) - } - - fn load_u32( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_u32(self, addr, metering, kind) - } - - fn load_byte( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_byte(self, addr, metering, kind) - } - - fn load_halfword( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_halfword(self, addr, metering, kind) - } - - fn load_word( - &self, - addr: usize, - metering: &mut dyn Metering, - kind: MemoryAccessKind, - ) -> Option { - MemoryPage::load_word(self, addr, metering, kind) - } - - fn write_code(&self, start_addr: usize, code: &[u8]) { - MemoryPage::write_code(self, start_addr, code) - } - - fn alloc_on_heap(&self, data: &[u8]) -> u32 { - MemoryPage::alloc_on_heap(self, data) - } - - fn stack_top(&self) -> u32 { - MemoryPage::stack_top(self) - } - - fn size(&self) -> usize { - MemoryPage::size(self) - } - - fn offset(&self, addr: usize) -> usize { - MemoryPage::offset(self, addr) - } - - fn next_heap(&self) -> u32 { - self.next_heap.get() - } - - fn set_next_heap(&self, next: u32) { - self.next_heap.set(next); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use vm::metering::NoopMeter; - - #[test] - fn test_offset_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - let mut meter = NoopMeter::default(); - assert_eq!(mem.offset(0), 0); - assert_eq!(mem.offset(100), 100); - assert_eq!(mem.offset(1023), 1023); - assert_eq!(mem.load_u32(0, &mut meter, MemoryAccessKind::Load), Some(0)); - } - - #[test] - fn test_offset_high_base() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - assert_eq!(mem.offset(base), 0); - assert_eq!(mem.offset(base + 100), 100); - assert_eq!(mem.offset(base + 1023), 1023); - } - - #[test] - #[should_panic(expected = "Address below base_address")] - fn test_offset_below_base_panics() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - mem.offset(base - 1); - } -} diff --git a/crates/avm/src/memory/memory_page_manager.rs b/crates/avm/src/memory/memory_page_manager.rs deleted file mode 100644 index caf1467..0000000 --- a/crates/avm/src/memory/memory_page_manager.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::rc::Rc; -use crate::memory::MemoryPage; -use vm::memory::Memory; - -#[derive(Debug)] -pub struct MemoryPageManager { - pub page_size: usize, - max_pages: usize, - pages: Vec, -} - -impl MemoryPageManager { - pub fn new(max_pages: usize, page_size: usize) -> Self { - assert!(max_pages != 0, "Max pages == 0"); - assert!(page_size != 0, "Page size == 0"); - - Self { - page_size, - max_pages, - pages: Vec::with_capacity(max_pages), - } - } - - /// Creates and owns a new page. Returns a mutable reference to it. - pub fn new_page(&mut self) -> Memory { - if self.pages.len() >= self.max_pages { - panic!( - "Out of memory: maximum page count ({}) reached", - self.max_pages - ); - } - - let page: Memory = Rc::new(MemoryPage::new(self.page_size)); - self.pages.push(Rc::clone(&page)); - return page; - } - - pub fn pop_page(&mut self) { - self.pages.pop(); - } - - /// Pretty-prints all memory pages linearly, indicating page boundaries - pub fn dump_all_pages_linear(&self) { - println!("Dumping memory ({} pages):", self.pages.len()); - for (i, page_rc) in self.pages.iter().enumerate() { - println!("\n=== Page {} ===", i); - - let mem = page_rc.mem(); - - for (j, chunk) in mem.chunks(16).enumerate() { - print!("0x{:04x}: ", j * 16); - - // Print hex representation - for byte in chunk { - print!("{:02x} ", byte); - } - - // Pad spacing if chunk is less than 16 bytes - for _ in chunk.len()..16 { - print!(" "); - } - - // Print ASCII representation - print!(" |"); - for byte in chunk { - let ch = *byte; - let display_char = if ch.is_ascii_graphic() || ch == b' ' { - ch as char - } else { - '.' - }; - print!("{}", display_char); - } - println!("|"); - } - } - } - - pub fn get_page(&self, index: usize) -> Option { - self.pages.get(index).cloned() // ✅ clone the Rc (increases refcount) - } - - pub fn first_page(&self) -> Option { - self.pages.first().cloned() // ✅ clone the Rc (increases refcount) - } - - pub fn top_page(&self) -> Option { - self.pages.last().cloned() // ✅ clone the Rc (increases refcount) - } -} diff --git a/crates/avm/src/memory/mod.rs b/crates/avm/src/memory/mod.rs deleted file mode 100644 index 6da46d1..0000000 --- a/crates/avm/src/memory/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod memory_page; -pub mod memory_page_manager; - -pub use memory_page::MemoryPage; -pub use memory_page_manager::MemoryPageManager; diff --git a/crates/avm/src/metering.rs b/crates/avm/src/metering.rs deleted file mode 100644 index d267c8e..0000000 --- a/crates/avm/src/metering.rs +++ /dev/null @@ -1,266 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use vm::instruction::Instruction; -use vm::metering::{MemoryAccessKind, MeterResult, Metering}; -use vm::sys_call::{ - SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, - SYSCALL_LOG, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, -}; - -/// Gas pricing table inspired by the EVM schedule: -/// - Storage operations are expensive (SLOAD/SSTORE style) -/// - External calls and value transfers carry a base "call" charge -/// - Memory copies and logs are charged per byte like calldata/logdata gas -#[derive(Debug, Clone, Copy)] -pub struct GasSchedule { - pub instruction: u64, - pub memory_load_base: u64, - pub memory_store_base: u64, - pub memory_atomic_base: u64, - pub memory_res_load_base: u64, - pub memory_res_store_base: u64, - pub memory_byte_cost: u64, - pub register_read: u64, - pub register_write: u64, - pub pc_update: u64, - pub syscall_base: u64, - pub syscall_storage_get: u64, - pub syscall_storage_set: u64, - pub syscall_log: u64, - pub syscall_call_program: u64, - pub syscall_fire_event: u64, - pub syscall_alloc: u64, - pub syscall_dealloc: u64, - pub syscall_transfer: u64, - pub syscall_balance: u64, - pub call_base: u64, - pub call_data_byte: u64, - pub log_data_byte: u64, - pub storage_key_byte: u64, - pub storage_value_byte: u64, - pub alloc_word: u64, - pub alloc_base: u64, -} - -impl Default for GasSchedule { - fn default() -> Self { - // Costs take cues from Ethereum (London): - // - CALL base ~700, value transfer ~9000, cold storage access ~2100, SSTORE ~20k - // - Calldata/log data charged per byte; memory growth charged per word - Self { - instruction: 1, - memory_load_base: 3, - memory_store_base: 5, - memory_atomic_base: 25, - memory_res_load_base: 12, - memory_res_store_base: 18, - memory_byte_cost: 1, - register_read: 0, - register_write: 0, - pc_update: 0, - syscall_base: 30, - syscall_storage_get: 2100, - syscall_storage_set: 20_000, - syscall_log: 375, - syscall_call_program: 40, // bulk of the cost is charged via call_base/call_data_byte - syscall_fire_event: 375, - syscall_alloc: 15, - syscall_dealloc: 4, - syscall_transfer: 9000, - syscall_balance: 2600, - call_base: 700, - call_data_byte: 4, - log_data_byte: 8, - storage_key_byte: 4, - storage_value_byte: 16, - alloc_word: 3, - alloc_base: 15, - } - } -} - -impl GasSchedule { - fn memory_cost(&self, kind: MemoryAccessKind, bytes: usize) -> u64 { - let per_byte = self.memory_byte_cost.saturating_mul(bytes as u64); - let base = match kind { - MemoryAccessKind::Load => self.memory_load_base, - MemoryAccessKind::Store => self.memory_store_base, - MemoryAccessKind::Atomic => self.memory_atomic_base, - MemoryAccessKind::ReservationLoad => self.memory_res_load_base, - MemoryAccessKind::ReservationStore => self.memory_res_store_base, - }; - base.saturating_add(per_byte) - } - - fn syscall_cost(&self, call_id: u32) -> u64 { - let specific = match call_id { - SYSCALL_STORAGE_GET => self.syscall_storage_get, - SYSCALL_STORAGE_SET => self.syscall_storage_set, - SYSCALL_LOG => self.syscall_log, - SYSCALL_CALL_PROGRAM => self.syscall_call_program, - SYSCALL_FIRE_EVENT => self.syscall_fire_event, - SYSCALL_ALLOC => self.syscall_alloc, - SYSCALL_DEALLOC => self.syscall_dealloc, - SYSCALL_TRANSFER => self.syscall_transfer, - SYSCALL_BALANCE => self.syscall_balance, - _ => 0, - }; - self.syscall_base.saturating_add(specific) - } - - fn syscall_data_cost(&self, call_id: u32, bytes: usize) -> u64 { - let per_byte = match call_id { - SYSCALL_STORAGE_GET => self.storage_key_byte, - SYSCALL_STORAGE_SET => self.storage_value_byte, - SYSCALL_LOG | SYSCALL_FIRE_EVENT => self.log_data_byte, - SYSCALL_TRANSFER | SYSCALL_BALANCE => self.storage_key_byte, - _ => self.call_data_byte, - }; - per_byte.saturating_mul(bytes as u64) - } - - fn alloc_cost(&self, bytes: usize) -> u64 { - let words = ((bytes as u64).saturating_add(31)) / 32; - self.alloc_base - .saturating_add(self.alloc_word.saturating_mul(words.max(1))) - } - - fn call_cost(&self, input_bytes: usize) -> u64 { - self.call_base - .saturating_add(self.call_data_byte.saturating_mul(input_bytes as u64)) - } -} - -/// Core gas accounting state shared across nested contract calls. -#[derive(Debug)] -pub struct GasMeter { - schedule: GasSchedule, - gas_used: u64, -} - -impl GasMeter { - pub fn new() -> Self { - Self { - schedule: GasSchedule::default(), - gas_used: 0, - } - } - - pub fn used(&self) -> u64 { - self.gas_used - } - - pub fn charge_call(&mut self, input_bytes: usize) -> MeterResult { - self.consume(self.schedule.call_cost(input_bytes)) - } - - fn consume(&mut self, amount: u64) -> MeterResult { - if amount == 0 { - return MeterResult::Continue; - } - - self.gas_used = self.gas_used.saturating_add(amount); - MeterResult::Continue - } - - fn charge_instruction(&mut self) -> MeterResult { - self.consume(self.schedule.instruction) - } - - fn charge_memory(&mut self, kind: MemoryAccessKind, bytes: usize) -> MeterResult { - self.consume(self.schedule.memory_cost(kind, bytes)) - } - - fn charge_syscall(&mut self, call_id: u32) -> MeterResult { - self.consume(self.schedule.syscall_cost(call_id)) - } - - fn charge_syscall_data(&mut self, call_id: u32, bytes: usize) -> MeterResult { - self.consume(self.schedule.syscall_data_cost(call_id, bytes)) - } - - fn charge_register_read(&mut self) -> MeterResult { - self.consume(self.schedule.register_read) - } - - fn charge_register_write(&mut self) -> MeterResult { - self.consume(self.schedule.register_write) - } - - fn charge_pc_update(&mut self) -> MeterResult { - self.consume(self.schedule.pc_update) - } - - fn charge_alloc(&mut self, bytes: usize) -> MeterResult { - self.consume(self.schedule.alloc_cost(bytes)) - } -} - -/// Thin adapter that lets the VM/CPU hold a boxed metering implementation -/// while multiple VMs share the same underlying gas counter. -#[derive(Clone)] -pub struct SharedGasMeter { - inner: Rc>, -} - -impl SharedGasMeter { - pub fn new(inner: Rc>) -> Self { - Self { inner } - } - - pub fn used(&self) -> u64 { - self.inner.borrow().used() - } -} - -impl std::fmt::Debug for SharedGasMeter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let borrowed = self.inner.borrow(); - f.debug_struct("SharedGasMeter") - .field("used", &borrowed.used()) - .finish() - } -} - -impl Metering for SharedGasMeter { - fn on_instruction(&mut self, _pc: u32, _instr: &Instruction, _size: u8) -> MeterResult { - self.inner.borrow_mut().charge_instruction() - } - - fn on_memory_access( - &mut self, - kind: MemoryAccessKind, - _addr: usize, - bytes: usize, - ) -> MeterResult { - self.inner.borrow_mut().charge_memory(kind, bytes) - } - - fn on_syscall(&mut self, call_id: u32, _args: &[u32; 6]) -> MeterResult { - self.inner.borrow_mut().charge_syscall(call_id) - } - - fn on_syscall_data(&mut self, call_id: u32, bytes: usize) -> MeterResult { - self.inner.borrow_mut().charge_syscall_data(call_id, bytes) - } - - fn on_register_read(&mut self, _reg: usize) -> MeterResult { - self.inner.borrow_mut().charge_register_read() - } - - fn on_register_write(&mut self, _reg: usize) -> MeterResult { - self.inner.borrow_mut().charge_register_write() - } - - fn on_pc_update(&mut self, _old_pc: u32, _new_pc: u32) -> MeterResult { - self.inner.borrow_mut().charge_pc_update() - } - - fn on_alloc(&mut self, bytes: usize) -> MeterResult { - self.inner.borrow_mut().charge_alloc(bytes) - } - - fn on_call(&mut self, input_bytes: usize) -> MeterResult { - self.inner.borrow_mut().charge_call(input_bytes) - } -} diff --git a/crates/avm/src/receipt.rs b/crates/avm/src/receipt.rs deleted file mode 100644 index 9ab4655..0000000 --- a/crates/avm/src/receipt.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::transaction::Transaction; -use types::Result; - -/// Represents the result of a transaction execution. -#[derive(Debug, Clone)] -pub struct TransactionReceipt { - /// Hash of the transaction. - pub tx: Transaction, - - /// Cumulative gas used in the block including this transaction. - // pub cumulative_gas_used: u64, - - // /// Gas used by this transaction alone. - // pub gas_used: u64, - pub result: Result, - - /// List of log entries generated during execution. - pub events: Vec>, -} - -impl TransactionReceipt { - /// Creates a new TransactionReceipt. - pub fn new(tx: Transaction, result: Result) -> Self { - TransactionReceipt { - tx, - // cumulative_gas_used: 0, - // gas_used: 0, - result, - events: Vec::new(), - } - } - - /// Adds an event to the receipt. - pub fn add_event(&mut self, event: Vec) -> &TransactionReceipt { - self.events.push(event); - self - } - - /// Optionally add multiple events at once. - pub fn set_events(mut self, events: Vec>) -> Self { - self.events = events; - self - } -} - -use core::fmt; - -impl fmt::Display for TransactionReceipt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "=== Transaction Receipt ===")?; - writeln!(f, "From: {:?}", self.tx.from)?; - writeln!(f, "To: {:?}", self.tx.to)?; - writeln!(f, "Result: {:?}", self.result)?; - writeln!(f, "Events:")?; - - for (i, event) in self.events.iter().enumerate() { - let hex = event - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(" "); - writeln!(f, " [{}] {}", i, hex)?; - } - - Ok(()) - } -} - -use compiler::{EventAbi, ParamType}; - -impl TransactionReceipt { - pub fn print_events_pretty(&self, abi_registry: &Vec, writer: &mut dyn fmt::Write) { - if self.events.is_empty() { - let _ = writeln!(writer, "No events in receipt."); - return; - } - - for event in &self.events { - Self::pretty_print_event(event, abi_registry, writer); - } - let _ = writeln!(writer); - } - - pub fn pretty_print_event( - event: &[u8], - abi_registry: &Vec, - writer: &mut dyn fmt::Write, - ) { - if event.len() < 32 { - let _ = writeln!(writer, "Invalid event: too short"); - return; - } - - let mut id = [0u8; 32]; - id.copy_from_slice(&event[..32]); - let data = &event[32..]; - - if let Some(abi) = abi_registry.iter().find(|abi| abi.id() == id) { - let _ = writeln!(writer, " {}: (", abi.name); - let mut offset = 0; - - let _ = writeln!(writer, " ID: 0x{}", hex::encode(id)); - for (i, param) in abi.inputs.iter().enumerate() { - let val = if param.indexed { - "".to_string() - } else { - match param.kind { - ParamType::Address => { - if offset + 20 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let bytes: &[u8] = &data[offset..offset + 20]; - offset += 20; - format!("0x{}", hex::encode(bytes)) - } - ParamType::Uint(256) => { - if offset + 32 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let bytes = &data[offset..offset + 32]; - offset += 32; - format!("0x{}", hex::encode(bytes)) - } - ParamType::Uint(128) => { - if offset + 16 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let bytes = &data[offset..offset + 16]; - offset += 16; - let raw = u128::from_le_bytes(bytes.try_into().unwrap()); - format!("{}", raw) - } - ParamType::Uint(64) => { - if offset + 8 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let bytes = &data[offset..offset + 8]; - offset += 8; - let raw = u64::from_le_bytes(bytes.try_into().unwrap()); - format!("{}", raw) - } - ParamType::Uint(32) => { - if offset + 4 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let bytes = &data[offset..offset + 4]; - offset += 4; - let raw = u32::from_le_bytes(bytes.try_into().unwrap()); - format!("{}", raw) - } - ParamType::Bool => { - if offset + 1 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let b = data[offset]; - offset += 1; - format!("{}", b != 0) - } - ParamType::Bytes => { - if offset + 1 > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let len = data[offset] as usize; - offset += 1; - if offset + len > data.len() { - let _ = writeln!( - writer, - " {}: ", - param.name - ); - break; - } - let bytes = &data[offset..offset + len]; - offset += len; - format!("0x{}", hex::encode(bytes)) - } - _ => { - let _ = writeln!(writer, " {}: ", param.name); - break; - } - } - }; - - let comma = if i + 1 < abi.inputs.len() { "," } else { "" }; - let _ = writeln!(writer, "\t{}: {}{}", param.name, val, comma); - } - - let _ = writeln!(writer, " )"); - } else { - let _ = writeln!(writer, "Unknown event: 0x{}", hex::encode(id)); - } - } -} diff --git a/crates/avm/src/router.rs b/crates/avm/src/router.rs deleted file mode 100644 index 933661e..0000000 --- a/crates/avm/src/router.rs +++ /dev/null @@ -1,21 +0,0 @@ -/// Represents a function call input for the VM router -pub struct HostFuncCall { - pub selector: u8, - pub args: Vec, -} - -/// Encodes multiple function calls into a single buffer for the guest VM router. -pub fn encode_router_calls(calls: &[HostFuncCall]) -> Vec { - let mut encoded = Vec::new(); - - for call in calls { - let len = call.args.len(); - assert!(len <= 255, "argument too long for 1-byte length field"); - - encoded.push(call.selector); - encoded.push(len as u8); - encoded.extend_from_slice(&call.args); - } - - encoded -} diff --git a/crates/avm/src/transaction.rs b/crates/avm/src/transaction.rs deleted file mode 100644 index 9a733c8..0000000 --- a/crates/avm/src/transaction.rs +++ /dev/null @@ -1,3 +0,0 @@ -use types::address::Address; - -pub use types::transaction::{Transaction, TransactionBundle, TransactionType}; diff --git a/crates/avm/tests/allocator_test.rs b/crates/avm/tests/allocator_test.rs deleted file mode 100644 index 0052491..0000000 --- a/crates/avm/tests/allocator_test.rs +++ /dev/null @@ -1,147 +0,0 @@ -use avm::memory::MemoryPage; -use std::cell::RefCell; -use std::rc::Rc; -use storage::Storage; -use vm::host_interface; -use vm::metering::NoopMeter; -use vm::memory::Memory; -use bootloader::DefaultSyscallHandler; -use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; - -#[test] -fn test_allocator_syscalls() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut meter = NoopMeter::default(); - - // Test SYSCALL_ALLOC - let args = [1024, 8, 0, 0, 0, 0]; - let mut regs = [0u32; 32]; - let (result, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - &mut meter, - ); - - assert_ne!(result, 0); - - // Test SYSCALL_DEALLOC (no-op but should not crash) - let dealloc_args = [result, 1024, 0, 0, 0, 0]; - let (dealloc_result, _) = syscall_handler.handle_syscall( - SYSCALL_DEALLOC, - dealloc_args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - &mut meter, - ); - - assert_eq!(dealloc_result, 0); -} - -#[test] -fn test_multiple_allocations() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut regs = [0u32; 32]; - let mut meter = NoopMeter::default(); - - let mut pointers = Vec::new(); - - // Allocate multiple blocks - for i in 0..5 { - let size = 64 + i * 32; - let args = [size, 4, 0, 0, 0, 0]; - - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - &mut meter, - ); - - assert_ne!(ptr, 0); - pointers.push(ptr); - } - - // Verify pointers are aligned - for &ptr in &pointers { - assert_eq!(ptr % 4, 0); - } - - // Verify no overlapping pointers (simple check) - for i in 0..pointers.len() { - for j in i + 1..pointers.len() { - assert_ne!(pointers[i], pointers[j]); - } - } -} - -#[test] -fn test_alignment_requirements() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut regs = [0u32; 32]; - let mut meter = NoopMeter::default(); - - // Test various alignments - let alignments = [1, 2, 4, 8, 16]; - - for &align in &alignments { - let args = [256, align as u32, 0, 0, 0, 0]; - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - &mut meter, - ); - - assert_ne!(ptr, 0); - assert_eq!(ptr as usize % align, 0); - } -} - -#[test] -fn test_invalid_alignment() { - let memory: Memory = Rc::new(MemoryPage::new(8192)); - let storage = Rc::new(RefCell::new(Storage::new())); - let mut host: Box = Box::new(host_interface::NoopHost); - let mut syscall_handler = DefaultSyscallHandler::new(); - let mut regs = [0u32; 32]; - let mut meter = NoopMeter::default(); - - // Test invalid alignments (not powers of 2) - let invalid_alignments = [0, 3, 5, 6, 7, 9]; - - for &align in &invalid_alignments { - let args = [100, align as u32, 0, 0, 0, 0]; - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - memory.clone(), - storage.clone(), - &mut host, - &mut regs, - &mut meter, - ); - - assert_eq!(ptr, 0); - } -} diff --git a/crates/avm/tests/memory_page_offset.rs b/crates/avm/tests/memory_page_offset.rs deleted file mode 100644 index fc6e2c7..0000000 --- a/crates/avm/tests/memory_page_offset.rs +++ /dev/null @@ -1,84 +0,0 @@ -use avm::memory::MemoryPage; -use vm::metering::{MemoryAccessKind, NoopMeter}; - -#[test] -fn test_offset_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - let mut meter = NoopMeter::default(); - assert_eq!(mem.offset(0), 0); - assert_eq!(mem.offset(100), 100); - assert_eq!(mem.offset(1023), 1023); - assert_eq!(mem.load_u32(0, &mut meter, MemoryAccessKind::Load), Some(0)); -} - -#[test] -fn test_offset_high_base() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - assert_eq!(mem.offset(base), 0); - assert_eq!(mem.offset(base + 100), 100); - assert_eq!(mem.offset(base + 1023), 1023); -} - -#[test] -#[should_panic(expected = "Address below base_address")] -fn test_offset_below_base_panics() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - mem.offset(base - 1); -} - -#[test] -fn test_store_and_load_zero_base() { - let mem = MemoryPage::new_with_base(1024, 0); - let mut meter = NoopMeter::default(); - assert!(mem.store_u8(10, 0xAB, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_byte(10, &mut meter, MemoryAccessKind::Load), - Some(0xAB) - ); - assert!(mem.store_u16(20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_halfword(20, &mut meter, MemoryAccessKind::Load), - Some(0xCDEF) - ); - assert!(mem.store_u32(30, 0x12345678, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_u32(30, &mut meter, MemoryAccessKind::Load), - Some(0x12345678) - ); -} - -#[test] -fn test_store_and_load_high_base() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - let mut meter = NoopMeter::default(); - assert!(mem.store_u8(base + 10, 0xAB, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_byte(base + 10, &mut meter, MemoryAccessKind::Load), - Some(0xAB) - ); - assert!(mem.store_u16(base + 20, 0xCDEF, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_halfword(base + 20, &mut meter, MemoryAccessKind::Load), - Some(0xCDEF) - ); - assert!(mem.store_u32(base + 30, 0x12345678, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_u32(base + 30, &mut meter, MemoryAccessKind::Load), - Some(0x12345678) - ); -} - -#[test] -fn test_store_and_load_at_offset_zero() { - let base = 0x80000000; - let mem = MemoryPage::new_with_base(1024, base); - let mut meter = NoopMeter::default(); - assert!(mem.store_u8(base, 0xAA, &mut meter, MemoryAccessKind::Store)); - assert_eq!( - mem.load_byte(base, &mut meter, MemoryAccessKind::Load), - Some(0xAA) - ); -} diff --git a/crates/avm/tests/spec_runner.rs b/crates/avm/tests/spec_runner.rs deleted file mode 100644 index 79323c6..0000000 --- a/crates/avm/tests/spec_runner.rs +++ /dev/null @@ -1,283 +0,0 @@ -//! Standalone test runner for rv32ui-p-* ELF files from riscv-tests -//! Loads an ELF file, loads it into the VM, and runs it to completion. - -use std::io::Read; -use std::path::Path; -use avm::memory::MemoryPage; -use vm::vm::VM; -mod test_syscall_handler; -use test_syscall_handler::TestSyscallHandler; -use vm::memory::Memory; - -/// Tests that are skipped and the reasons why -const SKIPPED_TESTS: &[(&str, &str)] = &[ - ( - "fence_i", - "Requires self-modifying code support (writes instructions to memory and executes them)", - ), - ( - "ld_st", - "Contains 64-bit load/store instructions (ld/sd) that the 32-bit VM doesn't support", - ), - ( - "st_ld", - "Contains 64-bit store/load instructions (sd/ld) that the 32-bit VM doesn't support", - ), - ("lrsc", "LR/SC implementation needs improvement - causes infinite loops"), -]; - -/// Testing categories to run -const TESTING_CATEGORIES: &[&str] = &["ui", "um", "ua", "uc"]; - -/// Check if a test file should be skipped -fn should_skip_test(file_name: &str) -> Option<&str> { - for (test_name, reason) in SKIPPED_TESTS { - if file_name.ends_with(test_name) { - return Some(reason); - } - } - None -} - -/// Run a single test file -fn run_single_test(elf_path: &str) -> Result<(), Box> { - if !Path::new(elf_path).exists() { - println!("ELF file not found at {}, skipping...", elf_path); - return Ok(()); - } - - // Read ELF file - let mut file = std::fs::File::open(elf_path)?; - let mut elf_bytes = Vec::new(); - file.read_to_end(&mut elf_bytes)?; - - // Parse ELF - let elf = compiler::elf::parse_elf_from_bytes(&elf_bytes)?; - let (code, code_start) = elf - .get_flat_code() - .ok_or("No code section in ELF")?; - let (rodata, rodata_start) = elf - .get_flat_rodata() - .unwrap_or((vec![], usize::MAX as u64)); - - // Get .data section if it exists - let (data, data_start) = if let Some(data_section) = elf.get_section_by_name(".data") { - (data_section.data.to_vec(), data_section.addr as usize) - } else { - (vec![], usize::MAX) - }; - - // Find .tohost section - if let Some(tohost_section) = elf.get_section_by_name(".tohost") { - println!( - ".tohost section found at addr=0x{:x}, size=0x{:x}", - tohost_section.addr, tohost_section.size - ); - } else { - println!(".tohost section not found, skipping..."); - return Ok(()); - } - - // Set up VM memory (allocate enough to cover 0x80000000+) - let memory: Memory = std::rc::Rc::new(MemoryPage::new_with_base( - 0x20000, - 0x80000000, - )); // 128KB at 0x80000000 - println!( - "Loading code into VM: addr=0x{:x}, size=0x{:x}", - code_start, - code.len() - ); - - // Set up VM - let storage = std::rc::Rc::new(std::cell::RefCell::new(storage::Storage::default())); - let host: Box = - Box::new(vm::host_interface::NoopHost {}); - // When constructing the VM, use the test syscall handler: - let mut syscall_handler = Box::new(TestSyscallHandler::new()); - - // Set .tohost address if found - if let Some(tohost_section) = elf.get_section_by_name(".tohost") { - syscall_handler.set_tohost_addr(tohost_section.addr); - syscall_handler.set_memory(memory.clone()); - } - - // Move the handler into the VM, then extract it after run - let mut vm = VM::new_with_syscall_handler(memory.clone(), storage, host, syscall_handler); - vm.cpu.verbose = false; // Set to false to reduce output for multiple tests - vm.set_code(code_start as u32, code_start as u32, &code); - - if !rodata.is_empty() { - println!( - "Writing rodata to memory: addr=0x{:x}, size=0x{:x}", - rodata_start, rodata.len() - ); - memory.write_code(rodata_start as usize, &rodata); - } - - if !data.is_empty() { - println!( - "Writing data to memory: addr=0x{:x}, size=0x{:x}", - data_start, data.len() - ); - memory.write_code(data_start as usize, &data); - } - - // Run the VM - println!("Running test..."); - vm.raw_run(); - println!("Test completed."); - - Ok(()) -} - -/// Discover and collect test files for a specific category -fn collect_test_files(test_dir: &str, category: &str) -> (Vec, usize) { - let mut test_files = Vec::new(); - let mut skipped_count = 0; - - println!("Looking for files in: {}", test_dir); - println!("Category prefix: rv32{}p-", category); - - if let Ok(entries) = std::fs::read_dir(test_dir) { - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - if let Some(file_name) = path.file_name() { - if let Some(name_str) = file_name.to_str() { - // Include files that start with the category prefix and are not .dump files - let category_prefix = format!("rv32{}-p-", category); - if name_str.starts_with(&category_prefix) - && !path.is_dir() - && !name_str.ends_with(".dump") - { - // Check if this test should be skipped - if let Some(reason) = should_skip_test(name_str) { - println!("Skipping {}: {}", name_str, reason); - skipped_count += 1; - continue; - } - - test_files.push(path.to_string_lossy().to_string()); - } - } - } - } - } - } else { - println!("Failed to read directory: {}", test_dir); - } - - test_files.sort(); // Sort for consistent ordering - (test_files, skipped_count) -} - -/// Run all tests for a specific category -fn run_category_tests( - test_dir: &str, - category: &str, -) -> Result<(usize, usize, usize), Box> { - println!("\n=== Running {} category tests ===", category.to_uppercase()); - - let (test_files, skipped_count) = collect_test_files(test_dir, category); - println!( - "Found {} {} test files to run ({} skipped)", - test_files.len(), - category, - skipped_count - ); - - let mut passed_count = 0; - let mut failed_count = 0; - - for (i, elf_path) in test_files.iter().enumerate() { - let test_name = std::path::Path::new(elf_path) - .file_name() - .unwrap() - .to_str() - .unwrap(); - - print!("[{:2}/{:2}] {}: ", i + 1, test_files.len(), test_name); - - if let Err(e) = run_single_test(elf_path) { - println!("❌ FAILED - {}", e); - failed_count += 1; - return Err(e); - } else { - println!("✅ PASSED"); - passed_count += 1; - } - } - - println!("=== {} category tests completed ===", category.to_uppercase()); - Ok((passed_count, failed_count, skipped_count)) -} - -#[test] -fn test_riscv_spec() { - // Discover all test files in the riscv-tests directory - let test_dir = "tests/riscv-tests-install/share/riscv-tests/isa"; - - // Print current working directory for debugging - println!("Current dir: {:?}", std::env::current_dir().unwrap()); - println!("Looking for tests in: {}", test_dir); - - // Check if the test directory exists - if !Path::new(test_dir).exists() { - println!("Test directory not found at {}, skipping test", test_dir); - return; - } - - println!("\n🚀 Starting RISC-V Specification Test Suite"); - println!("{}", "=".repeat(60)); - - let mut total_passed = 0; - let mut total_failed = 0; - let mut total_skipped = 0; - let mut category_results = Vec::new(); - - // Run tests for each category - for category in TESTING_CATEGORIES { - match run_category_tests(test_dir, category) { - Ok((passed, failed, skipped)) => { - total_passed += passed; - total_failed += failed; - total_skipped += skipped; - category_results.push((category.to_string(), passed, failed, skipped)); - } - Err(e) => { - println!("❌ Failed to run {} category tests: {}", category, e); - panic!("Test suite failed"); - } - } - } - - // Print comprehensive summary - println!("\n📊 Test Suite Summary"); - println!("{}", "=".repeat(60)); - println!( - "{:<10} | {:<8} | {:<8} | {:<8}", - "Category", "Passed", "Failed", "Skipped" - ); - println!("{}", "-".repeat(60)); - for (category, passed, failed, skipped) in category_results { - println!( - "{:<10} | {:<8} | {:<8} | {:<8}", - category.to_uppercase(), - passed, - failed, - skipped - ); - } - println!("{}", "-".repeat(60)); - println!( - "TOTAL | {:<8} | {:<8} | {:<8}", - total_passed, total_failed, total_skipped - ); - - if total_failed == 0 { - println!("🎉 All tests passed! Great job!"); - } else { - panic!("❌ Some tests failed. Please review the results above."); - } -} diff --git a/crates/avm/tests/test_syscall_handler.rs b/crates/avm/tests/test_syscall_handler.rs deleted file mode 100644 index 2ac4e31..0000000 --- a/crates/avm/tests/test_syscall_handler.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::any::Any; -use std::cell::RefCell; -use std::rc::Rc; -use storage::Storage; -use vm::host_interface::HostInterface; -use vm::metering::Metering; -use vm::memory::Memory; -use vm::registers::Register; -use vm::sys_call::SyscallHandler; - -/// Map RISC-V test exit codes to test case numbers -/// Formula: exit_code = (TESTNUM << 1) | 1 -/// So: TESTNUM = (exit_code - 1) >> 1 -fn exit_code_to_test_num(exit_code: u32) -> Option { - if exit_code == 0 { - None // 0 means test passed - } else if exit_code % 2 == 1 { - Some((exit_code - 1) >> 1) - } else { - None // Even exit codes are not from RVTEST_FAIL - } -} - -#[derive(Debug)] -pub struct TestSyscallHandler { - tohost_addr: u64, - memory: Option, -} - -impl TestSyscallHandler { - pub fn new() -> Self { - Self { - tohost_addr: 0, - memory: None, - } - } - - /// Set the address of the .tohost section - pub fn set_tohost_addr(&mut self, addr: u64) { - self.tohost_addr = addr; - } - - /// Set the memory reference (needed to read .tohost) - pub fn set_memory(&mut self, memory: Memory) { - self.memory = Some(memory); - } -} - -pub const SYSCALL_TEST_DONE: u32 = 0; -pub const SYSCALL_TERMINATE: u32 = 93; - -impl SyscallHandler for TestSyscallHandler { - fn handle_syscall( - &mut self, - call_id: u32, - _args: [u32; 6], - memory: Memory, - _storage: Rc>, - _host: &mut Box, - regs: &mut [u32; 32], - _metering: &mut dyn Metering, - ) -> (u32, bool) { - let mut result = 0; - match call_id { - SYSCALL_TEST_DONE => { - // Read .tohost value - let mem_ref = self.memory.as_ref().unwrap_or(&memory); - let offset = mem_ref.offset(self.tohost_addr as usize); - let mem = mem_ref.mem(); - if offset + 8 <= mem.len() { - let tohost_val = - u64::from_le_bytes(mem[offset..offset + 8].try_into().unwrap()); - // Use .tohost value as the test result - result = tohost_val as u32; - } else { - panic!("[TestSyscallHandler] .tohost address out of bounds"); - } - if result == 0 { - return (result, true); - } - panic!("[spec-test] FAIL: .tohost value = 0x{:x}", result); - } - SYSCALL_TERMINATE => { - let exit_code = regs[Register::A0 as usize]; - if exit_code != 0 { - // Try to map exit code to test case number - if let Some(test_num) = exit_code_to_test_num(exit_code) { - panic!( - "[spec-test] FAIL: Test case {} failed (exit code {})", - test_num, exit_code - ); - } else { - panic!("[spec-test] FAIL: Test failed with exit code {}", exit_code); - } - } - return (exit_code, false); // halt VM - } - _ => { - panic!("Unknown syscall ID: {}", call_id); - } - } - } - fn as_any(&self) -> &dyn Any { - self - } -} diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 3dda445..f096316 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -10,7 +10,6 @@ use types::{boot::BootInfo, transaction::TransactionBundle, TransactionReceipt, use crate::DefaultSyscallHandler; use state::State; -use vm::host_interface::NoopHost; use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; use vm::registers::Register; use vm::vm::VM; @@ -142,11 +141,8 @@ impl Bootloader { verbose_writer: Option>>, ) -> Option> { let (entry_point, memory) = self.load_kernel(kernel_elf); - let host: Box = Box::new(NoopHost); - let mut vm = VM::new( memory.clone(), - host, Box::new(DefaultSyscallHandler::with_heap( state.clone(), Rc::clone(&self.heap_ptr), diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs index 4fd03c8..f462b43 100644 --- a/crates/bootloader/src/syscalls.rs +++ b/crates/bootloader/src/syscalls.rs @@ -4,8 +4,7 @@ use std::any::Any; use std::rc::Rc; use state::State; -use types::{ADDRESS_LEN, address::Address, result::RESULT_SIZE}; -use vm::host_interface::HostInterface; +use types::{ADDRESS_LEN, address::Address}; use vm::memory::{API, MMU, HEAP_PTR_OFFSET, Memory, Perms, VirtualAddress}; use vm::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; use vm::registers::Register; @@ -126,7 +125,6 @@ impl SyscallHandler for DefaultSyscallHandler { args: [u32; 6], caller_mode: vm::cpu::PrivilegeMode, memory: Memory, - host: &mut Box, regs: &mut [u32; 32], metering: &mut dyn Metering, ) -> (u32, bool) { @@ -138,12 +136,12 @@ impl SyscallHandler for DefaultSyscallHandler { SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, metering), SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), SYSCALL_LOG => self.sys_log(args, caller_mode, memory, metering), - SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, host, metering), - SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, host, metering), + SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, metering), + SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, metering), SYSCALL_ALLOC => self.sys_alloc(args, memory, metering), SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), - SYSCALL_TRANSFER => self.sys_transfer(args, memory, host, metering), - SYSCALL_BALANCE => self.sys_balance(args, memory, host, metering), + SYSCALL_TRANSFER => self.sys_transfer(args, memory, metering), + SYSCALL_BALANCE => self.sys_balance(args, memory, metering), SYSCALL_BRK => self.sys_brk(args, memory, metering), _ => { panic!("Unknown syscall: {}", call_id); @@ -161,7 +159,6 @@ impl DefaultSyscallHandler { &mut self, args: [u32; 6], memory: Memory, - host: &mut Box, metering: &mut dyn Metering, ) -> u32 { // EDUCATIONAL: Extract key pointer and length from arguments @@ -180,12 +177,11 @@ impl DefaultSyscallHandler { // EDUCATIONAL: Safely read the key from memory // EDUCATIONAL: Create a limited scope to avoid borrow checker issues let (start, end) = va_range(ptr, len); - let event_bytes = match borrowed_memory.mem_slice(start, end) { + let _event_bytes = match borrowed_memory.mem_slice(start, end) { Some(r) => r, None => panic!("invalid memory access"), // Invalid memory access }; - host.fire_event(event_bytes.to_vec()); 0 } @@ -694,56 +690,35 @@ impl DefaultSyscallHandler { &mut self, args: [u32; 6], memory: Memory, - host: &mut Box, metering: &mut dyn Metering, ) -> u32 { let to_ptr = args[0] as usize; let from_ptr = args[1] as usize; let input_ptr = args[2] as usize; let input_len = args[3] as usize; - let result_ptr: u32; - let page_index: usize; if matches!(metering.on_call(input_len), MeterResult::Halt) { panic!("Metering halted SYSCALL_CALL_PROGRAM"); } { let borrowed_memory = memory.as_ref(); let (to_start, to_end) = va_range(to_ptr, 20); - let to_slice = match borrowed_memory.mem_slice(to_start, to_end) { + let _to_slice = match borrowed_memory.mem_slice(to_start, to_end) { Some(r) => r, None => return 0, }; let (from_start, from_end) = va_range(from_ptr, 20); - let from_slice = match borrowed_memory.mem_slice(from_start, from_end) { + let _from_slice = match borrowed_memory.mem_slice(from_start, from_end) { Some(r) => r, None => return 0, }; let (input_start, input_end) = va_range(input_ptr, input_len); - let input_slice = match borrowed_memory.mem_slice(input_start, input_end) { + let _input_slice = match borrowed_memory.mem_slice(input_start, input_end) { Some(r) => r, None => return 0, }; - let mut to_bytes = [0u8; 20]; - let mut from_bytes = [0u8; 20]; - to_bytes.copy_from_slice(&to_slice); - from_bytes.copy_from_slice(&from_slice); - let input_vec = input_slice.to_vec(); - (result_ptr, page_index) = host.call_program(from_bytes, to_bytes, input_vec); - } - { - let borrowed_memory = memory.as_ref(); - let result_bytes = match host.read_memory_page(page_index, result_ptr, RESULT_SIZE) { - Some(b) => b, - None => return 0, - }; - if matches!(metering.on_alloc(result_bytes.len()), MeterResult::Halt) { - panic!("Metering halted alloc for call_program result"); - } - match self.alloc_on_heap(&memory, &result_bytes, 8) { - Some(ptr) => ptr.as_u32(), - None => 0, - } } + // Host integration removed; call_program is currently unsupported. + 0 } fn sys_alloc(&mut self, args: [u32; 6], memory: Memory, metering: &mut dyn Metering) -> u32 { @@ -793,7 +768,6 @@ impl DefaultSyscallHandler { &mut self, args: [u32; 6], memory: Memory, - host: &mut Box, metering: &mut dyn Metering, ) -> u32 { // args: a2=to ptr, a3=value_lo, a4=value_hi @@ -813,17 +787,17 @@ impl DefaultSyscallHandler { let (to_start, to_end) = va_range(to_ptr, 20); let to_slice = borrowed.mem_slice(to_start, to_end).expect("invalid to ptr"); - let mut to = [0u8; 20]; - to.copy_from_slice(to_slice.as_ref()); - - if host.transfer(to, value) { 0 } else { 1 } + let mut _to = [0u8; 20]; + _to.copy_from_slice(to_slice.as_ref()); + let _ = value; + // Host integration removed; transfer is currently unsupported. + 1 } fn sys_balance( &mut self, args: [u32; 6], memory: Memory, - host: &mut Box, metering: &mut dyn Metering, ) -> u32 { // args: a1 = address pointer (20 bytes) @@ -845,7 +819,12 @@ impl DefaultSyscallHandler { addr }; - let bal = host.balance(addr); + let bal = self + .state + .borrow() + .get_account(&Address(addr)) + .map(|acc| acc.balance) + .unwrap_or(0); match self.alloc_on_heap(&memory, &bal.to_le_bytes(), 8) { Some(ptr) => ptr.as_u32(), None => 0, diff --git a/crates/bootloader/tests/allocator_test.rs b/crates/bootloader/tests/allocator_test.rs index e892196..594eaeb 100644 --- a/crates/bootloader/tests/allocator_test.rs +++ b/crates/bootloader/tests/allocator_test.rs @@ -2,16 +2,14 @@ use bootloader::DefaultSyscallHandler; use state::State; use std::cell::RefCell; use std::rc::Rc; -use vm::host_interface; use vm::memory::{Memory, Sv32Memory, PAGE_SIZE}; use vm::metering::NoopMeter; -use vm::sys_call::{SyscallHandler, SYSCALL_ALLOC, SYSCALL_DEALLOC}; +use vm::sys_call::{SYSCALL_ALLOC, SYSCALL_DEALLOC}; #[test] fn test_allocator_syscalls() { let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); - let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut meter = NoopMeter::default(); @@ -23,7 +21,6 @@ fn test_allocator_syscalls() { args, vm::cpu::PrivilegeMode::Supervisor, memory.clone(), - &mut host, &mut regs, &mut meter, ); @@ -37,7 +34,6 @@ fn test_allocator_syscalls() { dealloc_args, vm::cpu::PrivilegeMode::Supervisor, memory.clone(), - &mut host, &mut regs, &mut meter, ); @@ -49,7 +45,6 @@ fn test_allocator_syscalls() { fn test_multiple_allocations() { let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); - let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut regs = [0u32; 32]; let mut meter = NoopMeter::default(); @@ -66,7 +61,6 @@ fn test_multiple_allocations() { args, vm::cpu::PrivilegeMode::Supervisor, memory.clone(), - &mut host, &mut regs, &mut meter, ); @@ -92,7 +86,6 @@ fn test_multiple_allocations() { fn test_alignment_requirements() { let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); - let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut regs = [0u32; 32]; let mut meter = NoopMeter::default(); @@ -107,7 +100,6 @@ fn test_alignment_requirements() { args, vm::cpu::PrivilegeMode::Supervisor, memory.clone(), - &mut host, &mut regs, &mut meter, ); @@ -121,7 +113,6 @@ fn test_alignment_requirements() { fn test_invalid_alignment() { let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); let state = Rc::new(RefCell::new(State::new())); - let mut host: Box = Box::new(host_interface::NoopHost); let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); let mut regs = [0u32; 32]; let mut meter = NoopMeter::default(); @@ -136,7 +127,6 @@ fn test_invalid_alignment() { args, vm::cpu::PrivilegeMode::Supervisor, memory.clone(), - &mut host, &mut regs, &mut meter, ); diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 81543c8..057cdad 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -1,5 +1,4 @@ use crate::decoder::{decode_compressed, decode_full}; -use crate::host_interface::HostInterface; use crate::instruction::Instruction; use crate::memory::{Memory, VirtualAddress}; use crate::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; @@ -287,7 +286,7 @@ impl CPU { /// worker (instruction) performs a specific task. The conveyor belt (PC) /// moves to the next task automatically, unless a task specifically /// redirects the flow (like a branch or jump instruction). - pub fn step(&mut self, memory: Memory, host: &mut Box) -> bool { + pub fn step(&mut self, memory: Memory) -> bool { // EDUCATIONAL: Step 1 - Fetch and decode the next instruction let instr = self.next_instruction(Rc::clone(&memory)); @@ -295,7 +294,7 @@ impl CPU { match instr { Some((instr, size)) => { // Valid instruction found - execute it - self.run_instruction(instr, size, Rc::clone(&memory), host) + self.run_instruction(instr, size, Rc::clone(&memory)) } None => { // No valid instruction found - handle the error @@ -322,7 +321,6 @@ impl CPU { instr: Instruction, size: u8, memory: Memory, - host: &mut Box, ) -> bool { // EDUCATIONAL: Debug output to help understand what's happening // Get the actual instruction bytes for debugging @@ -358,7 +356,7 @@ impl CPU { let old_pc = self.pc; // EDUCATIONAL: Execute the instruction - let result = self.execute(instr.clone(), memory, host); + let result = self.execute(instr.clone(), memory); if !result { self.log( &format!( diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index 8c20b00..de2adba 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,7 +1,6 @@ use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP, CSR_SEPC, SCAUSE_BREAKPOINT}; use crate::sys_call::SYSCALL_LOG; use crate::memory::VirtualAddress; -use crate::host_interface::HostInterface; use crate::instruction::CsrOp; use crate::registers::Register; @@ -29,7 +28,6 @@ impl CPU { &mut self, instr: Instruction, memory: Memory, - host: &mut Box, ) -> bool { match instr { // EDUCATIONAL: Arithmetic instructions - perform mathematical operations @@ -810,7 +808,6 @@ impl CPU { args, self.priv_mode, memory, - host, &mut self.regs, self.metering.as_mut(), ); diff --git a/crates/vm/src/host_interface.rs b/crates/vm/src/host_interface.rs deleted file mode 100644 index 82c48ec..0000000 --- a/crates/vm/src/host_interface.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::fmt::Debug; - -pub trait HostInterface: Debug { - // calls another program, returns result ptr and page index - fn call_program(&mut self, from: [u8; 20], to: [u8; 20], input_data: Vec) -> (u32, usize); - fn read_memory_page(&mut self, page_index: usize, guest_ptr: u32, len: usize) -> Option>; - fn fire_event(&mut self, event: Vec); - fn transfer(&mut self, to: [u8; 20], value: u64) -> bool; - fn balance(&mut self, addr: [u8; 20]) -> u128; -} - -#[derive(Debug)] -pub struct NoopHost; - -impl HostInterface for NoopHost { - fn call_program(&mut self, _from: [u8; 20], _to: [u8; 20], _input_data: Vec) -> (u32, usize) { - (0, 0) - } - fn read_memory_page(&mut self, _page_index: usize, _guest_ptr: u32, _len: usize) -> Option> { - None - } - fn fire_event(&mut self, _event: Vec) { - // No operation - } - fn transfer(&mut self, _to: [u8; 20], _value: u64) -> bool { - false - } - fn balance(&mut self, _addr: [u8; 20]) -> u128 { - 0 - } -} diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index adf4b84..1723d9c 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -1,6 +1,5 @@ pub mod cpu; pub mod decoder; -pub mod host_interface; pub mod instruction; pub mod isa; pub mod isa_compressed; diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs index 4dd16c5..c75cc8e 100644 --- a/crates/vm/src/sys_call.rs +++ b/crates/vm/src/sys_call.rs @@ -1,5 +1,4 @@ use crate::cpu::PrivilegeMode; -use crate::host_interface::HostInterface; use crate::memory::Memory; use crate::metering::Metering; use core::any::Any; @@ -25,7 +24,6 @@ pub trait SyscallHandler: std::fmt::Debug { args: [u32; 6], caller_mode: PrivilegeMode, memory: Memory, - host: &mut Box, regs: &mut [u32; 32], metering: &mut dyn Metering, ) -> (u32, bool); diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index f5de154..7a9a1fe 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -1,5 +1,4 @@ use crate::cpu::CPU; -use crate::host_interface::HostInterface; use crate::memory::{API, Memory}; use crate::metering::Metering; use crate::registers::Register; @@ -28,35 +27,25 @@ pub struct VM { /// Shared reference to the VM's memory (RAM) pub memory: Memory, - pub host: Box, } impl VM { - /// Creates a new virtual machine with the specified memory, host, and syscall handler. + /// Creates a new virtual machine with the specified memory and syscall handler. pub fn new( memory: Memory, - host: Box, syscall_handler: Box, ) -> Self { - Self::new_with_syscall_handler(memory, host, syscall_handler) + Self::new_with_syscall_handler(memory, syscall_handler) } /// Creates a new virtual machine with a custom syscall handler. /// This is useful for testing or custom environments. - pub fn new_with_syscall_handler( - memory: Memory, - host: Box, - syscall_handler: Box, - ) -> Self { + pub fn new_with_syscall_handler(memory: Memory, syscall_handler: Box) -> Self { let mut cpu = CPU::new(syscall_handler); cpu.regs[Register::Sp as usize] = memory.stack_top().as_u32(); let satp = memory.satp(); cpu.set_satp(&memory, satp); - Self { - cpu, - memory, - host, - } + Self { cpu, memory } } /// Installs a metering implementation on the underlying CPU. @@ -178,6 +167,6 @@ impl VM { /// call this after setting up the initial state. pub fn raw_run(&mut self) { // EDUCATIONAL: Main execution loop - fetch, decode, execute - while self.cpu.step(Rc::clone(&self.memory), &mut self.host) {} + while self.cpu.step(Rc::clone(&self.memory)) {} } } From 972b9ef06227e29729413b8722793ddb78809033 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 31 Dec 2025 13:25:34 +0200 Subject: [PATCH 52/70] Route console logging through kernel and remove VM syscall handling - add a VM console writer entry point and use ecall id 1000 for console output - drop VM syscall handling and host syscall module; simplify VM/CPU construction - remove bootloader syscall module and related tests - move syscall IDs into program, add kernel/guest log prefixes, and update deps --- crates/bootloader/Cargo.toml | 1 + crates/bootloader/src/bootloader.rs | 9 +- crates/bootloader/src/lib.rs | 6 - crates/bootloader/src/syscalls.rs | 857 ---------------------- crates/bootloader/tests/allocator_test.rs | 136 ---- crates/kernel/Cargo.toml | 2 +- crates/kernel/src/syscall/mod.rs | 32 +- crates/program/Cargo.toml | 1 + crates/program/src/lib.rs | 12 + crates/program/src/log.rs | 11 +- crates/program/src/syscalls.rs | 11 + crates/vm/src/console.rs | 229 ++++++ crates/vm/src/cpu.rs | 9 +- crates/vm/src/exe.rs | 44 +- crates/vm/src/lib.rs | 2 +- crates/vm/src/sys_call.rs | 31 - crates/vm/src/vm.rs | 16 +- 17 files changed, 304 insertions(+), 1105 deletions(-) delete mode 100644 crates/bootloader/src/syscalls.rs delete mode 100644 crates/bootloader/tests/allocator_test.rs create mode 100644 crates/program/src/syscalls.rs create mode 100644 crates/vm/src/console.rs delete mode 100644 crates/vm/src/sys_call.rs diff --git a/crates/bootloader/Cargo.toml b/crates/bootloader/Cargo.toml index ce9074c..ad9c6bc 100644 --- a/crates/bootloader/Cargo.toml +++ b/crates/bootloader/Cargo.toml @@ -15,6 +15,7 @@ vm = { path = "../vm" } state = { path = "../state" } goblin = "0.10" compiler = { path = "../compiler" } +program = { path = "../program" } [target.'cfg(target_arch = "riscv32")'.dependencies] program = { path = "../program", default-features = false } diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index f096316..bb0905b 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -8,7 +8,6 @@ use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; use types::{boot::BootInfo, transaction::TransactionBundle, TransactionReceipt, SV32_DIRECT_MAP_BASE}; -use crate::DefaultSyscallHandler; use state::State; use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; use vm::registers::Register; @@ -141,13 +140,7 @@ impl Bootloader { verbose_writer: Option>>, ) -> Option> { let (entry_point, memory) = self.load_kernel(kernel_elf); - let mut vm = VM::new( - memory.clone(), - Box::new(DefaultSyscallHandler::with_heap( - state.clone(), - Rc::clone(&self.heap_ptr), - )), - ); + let mut vm = VM::new(memory.clone()); vm.set_reg_u32(Register::Sp, KERNEL_STACK_TOP); vm.cpu.verbose = verbose; if let Some(writer) = verbose_writer { diff --git a/crates/bootloader/src/lib.rs b/crates/bootloader/src/lib.rs index 22bb9b3..0bc7a2d 100644 --- a/crates/bootloader/src/lib.rs +++ b/crates/bootloader/src/lib.rs @@ -9,9 +9,3 @@ pub mod bootloader; pub mod result; - -pub mod syscalls; - -pub use vm::sys_call; - -pub use syscalls::DefaultSyscallHandler; diff --git a/crates/bootloader/src/syscalls.rs b/crates/bootloader/src/syscalls.rs deleted file mode 100644 index f462b43..0000000 --- a/crates/bootloader/src/syscalls.rs +++ /dev/null @@ -1,857 +0,0 @@ -use core::cell::{Cell, RefCell}; -use core::fmt::Write; -use std::any::Any; -use std::rc::Rc; - -use state::State; -use types::{ADDRESS_LEN, address::Address}; -use vm::memory::{API, MMU, HEAP_PTR_OFFSET, Memory, Perms, VirtualAddress}; -use vm::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; -use vm::registers::Register; -use vm::sys_call::{ - SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, - SYSCALL_FIRE_EVENT, SYSCALL_LOG, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, - SYSCALL_TRANSFER, SyscallHandler, -}; - -/// Represents different types of arguments that can be passed to system calls. -/// -/// EDUCATIONAL: This enum demonstrates how to handle different data types -/// in system calls. In real operating systems, system calls need to handle -/// various data types safely. -#[allow(dead_code)] -enum Arg { - U32(u32), // 32-bit unsigned integer - F32(f32), // 32-bit floating point - Char(char), // Single character - Str(String), // String (owned) - Bytes(Vec), // Raw bytes -} - -pub struct DefaultSyscallHandler { - verbose_writer: Option>>, - state: Rc>, - heap_ptr: Rc>, -} - -impl std::fmt::Debug for DefaultSyscallHandler { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DefaultSyscallHandler") - .field( - "verbose_writer", - &self.verbose_writer.as_ref().map(|_| ""), - ) - .field("state", &"") - .finish() - } -} - -impl DefaultSyscallHandler { - fn ensure_heap_ptr(&self) -> u32 { - let current = self.heap_ptr.get(); - if current == 0 { - self.heap_ptr.set(HEAP_PTR_OFFSET); - HEAP_PTR_OFFSET - } else { - current - } - } - - fn set_heap_ptr(&self, next: u32) { - self.heap_ptr.set(next); - } - - fn write_bytes(&self, memory: &Memory, start: VirtualAddress, data: &[u8]) -> bool { - let mut meter = NoopMeter::default(); - for (idx, byte) in data.iter().enumerate() { - let addr = start.wrapping_add(idx as u32); - if !memory.store_u8(addr, *byte, &mut meter, MemoryAccessKind::Store) { - return false; - } - } - true - } - - fn alloc_on_heap( - &self, - memory: &Memory, - data: &[u8], - align: u32, - ) -> Option { - let mut addr = self.ensure_heap_ptr(); - let mask = align.checked_sub(1)?; - addr = addr.checked_add(mask)? & !mask; - let end = addr.checked_add(data.len() as u32)?; - let start = VirtualAddress(addr); - memory.map_range(start, data.len(), Perms::rw_kernel()); - if !data.is_empty() && !self.write_bytes(memory, start, data) { - return None; - } - self.set_heap_ptr(end); - Some(start) - } - pub fn new(state: Rc>) -> Self { - Self::with_writer_and_heap(state, None, Rc::new(Cell::new(0))) - } - - pub fn with_heap(state: Rc>, heap_ptr: Rc>) -> Self { - Self::with_writer_and_heap(state, None, heap_ptr) - } - - pub fn with_writer( - state: Rc>, - writer: Option>>, - ) -> Self { - Self::with_writer_and_heap(state, writer, Rc::new(Cell::new(0))) - } - - fn with_writer_and_heap( - state: Rc>, - writer: Option>>, - heap_ptr: Rc>, - ) -> Self { - Self { - verbose_writer: writer, - state, - heap_ptr, - } - } -} - -impl SyscallHandler for DefaultSyscallHandler { - fn handle_syscall( - &mut self, - call_id: u32, - args: [u32; 6], - caller_mode: vm::cpu::PrivilegeMode, - memory: Memory, - regs: &mut [u32; 32], - metering: &mut dyn Metering, - ) -> (u32, bool) { - if matches!(metering.on_syscall(call_id, &args), MeterResult::Halt) { - panic!("Metering halted syscall {}", call_id); - } - let result = match call_id { - SYSCALL_STORAGE_GET => self.sys_storage_get(args, memory, metering), - SYSCALL_STORAGE_SET => self.sys_storage_set(args, memory, metering), - SYSCALL_PANIC => self.sys_panic_with_message(regs, memory), - SYSCALL_LOG => self.sys_log(args, caller_mode, memory, metering), - SYSCALL_CALL_PROGRAM => self.sys_call_program(args, memory, metering), - SYSCALL_FIRE_EVENT => self.sys_fire_event(args, memory, metering), - SYSCALL_ALLOC => self.sys_alloc(args, memory, metering), - SYSCALL_DEALLOC => self.sys_dealloc(args, memory, metering), - SYSCALL_TRANSFER => self.sys_transfer(args, memory, metering), - SYSCALL_BALANCE => self.sys_balance(args, memory, metering), - SYSCALL_BRK => self.sys_brk(args, memory, metering), - _ => { - panic!("Unknown syscall: {}", call_id); - } - }; - (result, true) - } - fn as_any(&self) -> &dyn Any { - self - } -} - -impl DefaultSyscallHandler { - pub fn sys_fire_event( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - // EDUCATIONAL: Extract key pointer and length from arguments - let ptr = args[0] as usize; - let len = args[1] as usize; - - if matches!( - metering.on_syscall_data(SYSCALL_FIRE_EVENT, len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_FIRE_EVENT"); - } - - let borrowed_memory = memory.as_ref(); - - // EDUCATIONAL: Safely read the key from memory - // EDUCATIONAL: Create a limited scope to avoid borrow checker issues - let (start, end) = va_range(ptr, len); - let _event_bytes = match borrowed_memory.mem_slice(start, end) { - Some(r) => r, - None => panic!("invalid memory access"), // Invalid memory access - }; - - 0 - } - - fn sys_storage_get( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let address_ptr = args[0] as usize; - let domain_ptr = args[1] as usize; - let key_ptr = args[2] as usize; - let lens_packed = args[3] as usize; - let domain_len = lens_packed & 0xffff; - let key_len = lens_packed >> 16; - - let total_len = ADDRESS_LEN - .saturating_add(domain_len) - .saturating_add(key_len); - if matches!( - metering.on_syscall_data(SYSCALL_STORAGE_GET, total_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_STORAGE_GET"); - } - - let borrowed_memory = memory.as_ref(); - - // Parse address - let (address_start, address_end) = va_range(address_ptr, ADDRESS_LEN); - let address_slice_ref = match borrowed_memory.mem_slice(address_start, address_end) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid address memory access: ptr={}, len={}", - address_ptr, ADDRESS_LEN - ); - return 0; - } - }; - let address_bytes = address_slice_ref.as_ref(); - let address_hex = address_bytes - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); - let mut addr_arr = [0u8; ADDRESS_LEN]; - addr_arr.copy_from_slice(address_bytes); - let address = Address(addr_arr); - - // Parse domain - let domain_slice = { - let (domain_start, domain_end) = va_range(domain_ptr, domain_len); - let domain_slice_ref = match borrowed_memory.mem_slice(domain_start, domain_end) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid domain memory access: ptr={}, len={}", - domain_ptr, domain_len - ); - return 0; - } - }; - domain_slice_ref.as_ref().to_vec() - }; - let domain = match core::str::from_utf8(&domain_slice) { - Ok(s) => s, - Err(_) => { - println!( - "❌ Storage GET - Invalid UTF-8 in domain: {:?}", - domain_slice - ); - return 0; - } - }; - - // Parse key - let key_slice = { - let (key_start, key_end) = va_range(key_ptr, key_len); - let key_slice_ref = match borrowed_memory.mem_slice(key_start, key_end) { - Some(r) => r, - None => { - println!( - "❌ Storage GET - Invalid key memory access: ptr={}, len={}", - key_ptr, key_len - ); - return 0; - } - }; - key_slice_ref.as_ref().to_vec() - }; - // Convert binary key to hex string for storage - let key = key_slice - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); - - // Format key for display based on domain - let display_key = if domain == "P" { - // For persistent domain, try to display key as ASCII - match core::str::from_utf8(&key_slice) { - Ok(s) => s.to_string(), - Err(_) => key.clone(), // fallback to hex if not valid UTF-8 - } - } else { - // For other domains, show domain as ASCII and key as hex - format!("{}:{}", domain, key) - }; - - let value = { - let state_ref = self.state.borrow(); - state_ref - .get_account(&address) - .and_then(|acc| acc.storage.get(&format!("{}:{}", domain, key)).cloned()) - }; - - if let Some(value) = value { - let mut buf = (value.len() as u32).to_le_bytes().to_vec(); - buf.extend_from_slice(value.as_slice()); - if matches!(metering.on_alloc(buf.len()), MeterResult::Halt) { - panic!("Metering halted alloc during storage_get"); - } - let addr = match self.alloc_on_heap(&memory, &buf, 8) { - Some(ptr) => ptr, - None => return 0, - }; - println!( - "✅ Found value for address: '{}', domain: '{}', Key: '{}'", - address_hex, domain, display_key - ); - return addr.as_u32(); - } else { - println!( - "❌ No value found for address: '{}', domain: '{}', key: '{}'", - address_hex, domain, display_key - ); - 0 - } - } - - fn sys_storage_set( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let address_ptr = args[0] as usize; - let domain_ptr = args[1] as usize; - let key_ptr = args[2] as usize; - let lens_packed = args[3] as usize; - let val_ptr = args[4] as usize; - let val_len = args[5] as usize; - - let domain_len = lens_packed & 0xffff; - let key_len = lens_packed >> 16; - - let total_len = ADDRESS_LEN - .saturating_add(domain_len) - .saturating_add(key_len) - .saturating_add(val_len); - if matches!( - metering.on_syscall_data(SYSCALL_STORAGE_SET, total_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_STORAGE_SET"); - } - - let borrowed_memory = memory.as_ref(); - - // Parse address - let (address_start, address_end) = va_range(address_ptr, ADDRESS_LEN); - let address_slice_ref = match borrowed_memory.mem_slice(address_start, address_end) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid address memory access: ptr={}, len={}", - address_ptr, ADDRESS_LEN - ); - return 0; - } - }; - let address_bytes = address_slice_ref.as_ref(); - let address_hex = address_bytes - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); - let mut addr_arr = [0u8; ADDRESS_LEN]; - addr_arr.copy_from_slice(address_bytes); - let address = Address(addr_arr); - - // Parse domain - let (domain_start, domain_end) = va_range(domain_ptr, domain_len); - let domain_slice_ref = match borrowed_memory.mem_slice(domain_start, domain_end) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid domain memory access: ptr={}, len={}", - domain_ptr, domain_len - ); - return 0; - } - }; - let domain_slice = domain_slice_ref.as_ref(); - let domain = match core::str::from_utf8(domain_slice) { - Ok(s) => s, - Err(_) => { - println!( - "❌ Storage SET - Invalid UTF-8 in domain: {:?}", - domain_slice - ); - return 0; - } - }; - - // Parse key - let (key_start, key_end) = va_range(key_ptr, key_len); - let key_slice_ref = match borrowed_memory.mem_slice(key_start, key_end) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid key memory access: ptr={}, len={}", - key_ptr, key_len - ); - return 0; - } - }; - let key_slice = key_slice_ref.as_ref(); - // Convert binary key to hex string for storage - let key = key_slice - .iter() - .map(|b| format!("{:02x}", b)) - .collect::>() - .join(""); - - // Format key for display based on domain - let display_key = if domain == "P" { - // For persistent domain, try to display key as ASCII - match core::str::from_utf8(key_slice) { - Ok(s) => s.to_string(), - Err(_) => key.clone(), // fallback to hex if not valid UTF-8 - } - } else { - // For other domains, show domain as ASCII and key as hex - format!("{}:{}", domain, key) - }; - - // Parse value - let (val_start, val_end) = va_range(val_ptr, val_len); - let value_slice_ref = match borrowed_memory.mem_slice(val_start, val_end) { - Some(r) => r, - None => { - println!( - "❌ Storage SET - Invalid value memory access: ptr={}, len={}", - val_ptr, val_len - ); - return 0; - } - }; - let value_slice = value_slice_ref.as_ref(); - - println!( - "💾 Storage SET - Domain: '{}', Key: '{}', Value: {:?} ({} bytes)", - domain, - display_key, - value_slice, - value_slice.len() - ); - - let composite_key = format!("{}:{}", domain, key); - self.state - .borrow_mut() - .get_account_mut(&address) - .storage - .insert(composite_key, value_slice.to_vec()); - 0 - } - - fn sys_panic_with_message(&mut self, regs: &mut [u32; 32], memory: Memory) -> u32 { - let msg_ptr = regs[Register::A0 as usize] as usize; - let msg_len = regs[Register::A1 as usize] as usize; - let (msg_start, msg_end) = va_range(msg_ptr, msg_len); - let msg = memory - .mem_slice(msg_start, msg_end) - .map(|bytes| String::from_utf8_lossy(bytes.as_ref()).into_owned()) - .unwrap_or_else(|| "".to_string()); - panic!("🔥 Guest panic: {}", msg); - } - - fn sys_log( - &mut self, - args: [u32; 6], - caller_mode: vm::cpu::PrivilegeMode, - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; - let payload_len = fmt_len.saturating_add(arg_len) as usize; - if matches!( - metering.on_syscall_data(SYSCALL_LOG, payload_len), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_LOG"); - } - let borrowed_memory = memory.as_ref(); - let (fmt_start, fmt_end) = va_range(fmt_ptr as usize, fmt_len as usize); - let fmt_slice = match borrowed_memory.mem_slice(fmt_start, fmt_end) { - Some(s) => s, - None => { - println!("⚠️ invalid format string @ 0x{:08x}", fmt_ptr); - return 0; - } - }; - let fmt_bytes = fmt_slice.as_ref(); - let fmt = match core::str::from_utf8(fmt_bytes) { - Ok(s) => s, - Err(e) => { - println!("⚠️ invalid UTF-8 in format string"); - println!("📦 bytes: {:?}", fmt_bytes); - println!("❌ error: {}", e); - return 0; - } - }; - let (args_start, args_end) = va_range(arg_ptr as usize, arg_len as usize); - let args_bytes_slice = borrowed_memory.mem_slice(args_start, args_end); - let args_bytes_holder; - let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { - args_bytes_holder = slice; - args_bytes_holder.as_ref() - } else { - b"" - }; - let raw_args: Vec = args_bytes - .chunks_exact(4) - .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) - .collect(); - let mut args: Vec = Vec::new(); - let mut raw_iter = raw_args.into_iter(); - let mut chars = fmt.chars().peekable(); - while let Some(c) = chars.next() { - if c != '%' { - continue; - } - let spec: char = chars.next().unwrap_or('%'); - let mut next = || raw_iter.next().unwrap_or(0); - match spec { - 'd' | 'u' | 'x' => args.push(Arg::U32(next())), - 'f' => args.push(Arg::F32(f32::from_bits(next()))), - 'c' => args.push(Arg::Char(char::from_u32(next()).unwrap_or('?'))), - 's' => { - let ptr = next() as usize; - let len = next() as usize; - let (start, end) = va_range(ptr, len); - match borrowed_memory.mem_slice(start, end) { - Some(slice) => { - let s_ptr = core::str::from_utf8(slice.as_ref()); - args.push(match s_ptr { - Ok(s) => Arg::Str(s.to_string()), - Err(_) => Arg::Str("".to_string()), - }); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'b' => { - let ptr = next() as usize; - let len = next() as usize; - let (start, end) = va_range(ptr, len); - match borrowed_memory.mem_slice(start, end) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'a' => { - // Array of u32s - let ptr = next() as usize; - let len = next() as usize; - let byte_len = len * 4; // u32 is 4 bytes - let (start, end) = va_range(ptr, byte_len); - match borrowed_memory.mem_slice(start, end) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - 'A' => { - // Array of u8s - let ptr = next() as usize; - let len = next() as usize; - let (start, end) = va_range(ptr, len); - match borrowed_memory.mem_slice(start, end) { - Some(slice) => { - args.push(Arg::Bytes(slice.to_vec())); - } - None => { - args.push(Arg::Str("".to_string())); - } - } - } - _ => args.push(Arg::Str("".to_string())), - } - } - let mut output = String::new(); - let mut args_iter = args.iter(); - let mut fmt_chars = fmt.chars().peekable(); - while let Some(c) = fmt_chars.next() { - if c == '%' { - match fmt_chars.next() { - Some('d') | Some('u') => match args_iter.next() { - Some(Arg::U32(v)) => output.push_str(&format!("{}", *v as i32)), - _ => output.push_str(""), - }, - Some('x') => match args_iter.next() { - Some(Arg::U32(v)) => output.push_str(&format!("{:08x}", v)), - _ => output.push_str(""), - }, - Some('f') => match args_iter.next() { - Some(Arg::F32(f)) => output.push_str(&format!("{}", f)), - _ => output.push_str(""), - }, - Some('c') => match args_iter.next() { - Some(Arg::Char(c)) => output.push(*c), - _ => output.push_str(""), - }, - Some('s') => match args_iter.next() { - Some(Arg::Str(s)) => output.push_str(s), - _ => output.push_str(""), - }, - Some('b') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format bytes array nicely - output.push('['); - for (i, byte) in b.iter().enumerate() { - if i > 0 { - output.push_str(", "); - } - output.push_str(&format!("0x{:02x}", byte)); - } - output.push(']'); - } - _ => output.push_str(""), - }, - Some('a') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format u32 array (bytes interpreted as u32s) - output.push('['); - for (i, chunk) in b.chunks_exact(4).enumerate() { - if i > 0 { - output.push_str(", "); - } - let val = - u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); - output.push_str(&format!("{}", val)); - } - output.push(']'); - } - _ => output.push_str(""), - }, - Some('A') => match args_iter.next() { - Some(Arg::Bytes(b)) => { - // Format u8 array - output.push('['); - for (i, byte) in b.iter().enumerate() { - if i > 0 { - output.push_str(", "); - } - output.push_str(&format!("{}", byte)); - } - output.push(']'); - } - _ => output.push_str(""), - }, - Some('%') => output.push('%'), - Some(_) | None => output.push_str("<%?>"), - } - } else { - output.push(c); - } - } - let prefix = match caller_mode { - vm::cpu::PrivilegeMode::Supervisor => "🛡️ Kernel", - vm::cpu::PrivilegeMode::User => "📜 Guest", - }; - match &self.verbose_writer { - Some(writer) => { - let _ = writeln!(writer.borrow_mut(), "{}: {}", prefix, output); - } - None => { - println!("{}: {}", prefix, output); - } - } - 0 - } - - fn sys_call_program( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - let to_ptr = args[0] as usize; - let from_ptr = args[1] as usize; - let input_ptr = args[2] as usize; - let input_len = args[3] as usize; - if matches!(metering.on_call(input_len), MeterResult::Halt) { - panic!("Metering halted SYSCALL_CALL_PROGRAM"); - } - { - let borrowed_memory = memory.as_ref(); - let (to_start, to_end) = va_range(to_ptr, 20); - let _to_slice = match borrowed_memory.mem_slice(to_start, to_end) { - Some(r) => r, - None => return 0, - }; - let (from_start, from_end) = va_range(from_ptr, 20); - let _from_slice = match borrowed_memory.mem_slice(from_start, from_end) { - Some(r) => r, - None => return 0, - }; - let (input_start, input_end) = va_range(input_ptr, input_len); - let _input_slice = match borrowed_memory.mem_slice(input_start, input_end) { - Some(r) => r, - None => return 0, - }; - } - // Host integration removed; call_program is currently unsupported. - 0 - } - - fn sys_alloc(&mut self, args: [u32; 6], memory: Memory, metering: &mut dyn Metering) -> u32 { - let size = args[0] as usize; // A0 register - let align = args[1] as usize; // A1 register - - if matches!(metering.on_alloc(size), MeterResult::Halt) { - panic!("Metering halted SYSCALL_ALLOC"); - } - - if size == 0 { - println!("VM Alloc: Invalid size 0"); - return 0; - } - - // Validate alignment (must be power of 2) - if align == 0 || (align & (align - 1)) != 0 { - println!("VM Alloc: Invalid alignment {}", align); - return 0; - } - - // Allocate aligned memory on heap - let data = vec![0u8; size]; - let ptr = match self.alloc_on_heap(&memory, &data, align as u32) { - Some(ptr) => ptr, - None => { - println!("VM Alloc: Out of memory, failed to allocate {} bytes", size); - return 0; - } - }; - - ptr.as_u32() - } - - fn sys_dealloc(&mut self, args: [u32; 6], _memory: Memory, metering: &mut dyn Metering) -> u32 { - let size = args[1] as usize; - if matches!(metering.on_alloc(size), MeterResult::Halt) { - panic!("Metering halted SYSCALL_DEALLOC"); - } - // Note: This VM uses a simple bump allocator, so we can't actually free memory - // In a real VM, you'd implement a proper allocator with free lists - // For now, this is a no-op since the memory will be reclaimed when the VM exits - 0 - } - - fn sys_transfer( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - // args: a2=to ptr, a3=value_lo, a4=value_hi - let to_ptr = args[1] as usize; - let value_lo = args[2] as u64; - let value_hi = args[3] as u64; - let value = value_lo | (value_hi << 32); - - if matches!( - metering.on_syscall_data(SYSCALL_TRANSFER, 20), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_TRANSFER"); - } - - let borrowed = memory.as_ref(); - let (to_start, to_end) = va_range(to_ptr, 20); - let to_slice = borrowed.mem_slice(to_start, to_end).expect("invalid to ptr"); - - let mut _to = [0u8; 20]; - _to.copy_from_slice(to_slice.as_ref()); - let _ = value; - // Host integration removed; transfer is currently unsupported. - 1 - } - - fn sys_balance( - &mut self, - args: [u32; 6], - memory: Memory, - metering: &mut dyn Metering, - ) -> u32 { - // args: a1 = address pointer (20 bytes) - let addr_ptr = args[0] as usize; - if matches!( - metering.on_syscall_data(SYSCALL_BALANCE, 20), - MeterResult::Halt - ) { - panic!("Metering halted SYSCALL_BALANCE"); - } - let addr = { - let borrowed = memory.as_ref(); - let (addr_start, addr_end) = va_range(addr_ptr, 20); - let addr_slice = borrowed - .mem_slice(addr_start, addr_end) - .expect("invalid addr ptr"); - let mut addr = [0u8; 20]; - addr.copy_from_slice(addr_slice.as_ref()); - addr - }; - - let bal = self - .state - .borrow() - .get_account(&Address(addr)) - .map(|acc| acc.balance) - .unwrap_or(0); - match self.alloc_on_heap(&memory, &bal.to_le_bytes(), 8) { - Some(ptr) => ptr.as_u32(), - None => 0, - } - } - - /// Minimal `brk(2)` implementation: - /// - a0 = new_break; if 0, return current break. - /// - Only moves the break forward; shrink requests are ignored. - fn sys_brk(&mut self, args: [u32; 6], memory: Memory, _metering: &mut dyn Metering) -> u32 { - let new_brk = args[0]; - let current = self.ensure_heap_ptr(); - if new_brk == 0 { - return current; - } - if new_brk >= current { - self.set_heap_ptr(new_brk); - new_brk - } else { - current - } - } - -} - -fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { - let start = VirtualAddress(ptr as u32); - let end = start.wrapping_add(len as u32); - (start, end) -} diff --git a/crates/bootloader/tests/allocator_test.rs b/crates/bootloader/tests/allocator_test.rs deleted file mode 100644 index 594eaeb..0000000 --- a/crates/bootloader/tests/allocator_test.rs +++ /dev/null @@ -1,136 +0,0 @@ -use bootloader::DefaultSyscallHandler; -use state::State; -use std::cell::RefCell; -use std::rc::Rc; -use vm::memory::{Memory, Sv32Memory, PAGE_SIZE}; -use vm::metering::NoopMeter; -use vm::sys_call::{SYSCALL_ALLOC, SYSCALL_DEALLOC}; - -#[test] -fn test_allocator_syscalls() { - let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); - let state = Rc::new(RefCell::new(State::new())); - let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); - let mut meter = NoopMeter::default(); - - // Test SYSCALL_ALLOC - let args = [1024, 8, 0, 0, 0, 0]; - let mut regs = [0u32; 32]; - let (result, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - vm::cpu::PrivilegeMode::Supervisor, - memory.clone(), - &mut regs, - &mut meter, - ); - - assert_ne!(result, 0); - - // Test SYSCALL_DEALLOC (no-op but should not crash) - let dealloc_args = [result, 1024, 0, 0, 0, 0]; - let (dealloc_result, _) = syscall_handler.handle_syscall( - SYSCALL_DEALLOC, - dealloc_args, - vm::cpu::PrivilegeMode::Supervisor, - memory.clone(), - &mut regs, - &mut meter, - ); - - assert_eq!(dealloc_result, 0); -} - -#[test] -fn test_multiple_allocations() { - let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); - let state = Rc::new(RefCell::new(State::new())); - let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); - let mut regs = [0u32; 32]; - let mut meter = NoopMeter::default(); - - let mut pointers = Vec::new(); - - // Allocate multiple blocks - for i in 0..5 { - let size = 64 + i * 32; - let args = [size, 4, 0, 0, 0, 0]; - - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - vm::cpu::PrivilegeMode::Supervisor, - memory.clone(), - &mut regs, - &mut meter, - ); - - assert_ne!(ptr, 0); - pointers.push(ptr); - } - - // Verify pointers are aligned - for &ptr in &pointers { - assert_eq!(ptr % 4, 0); - } - - // Verify no overlapping pointers (simple check) - for i in 0..pointers.len() { - for j in i + 1..pointers.len() { - assert_ne!(pointers[i], pointers[j]); - } - } -} - -#[test] -fn test_alignment_requirements() { - let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); - let state = Rc::new(RefCell::new(State::new())); - let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); - let mut regs = [0u32; 32]; - let mut meter = NoopMeter::default(); - - // Test various alignments - let alignments = [1, 2, 4, 8, 16]; - - for &align in &alignments { - let args = [256, align as u32, 0, 0, 0, 0]; - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - vm::cpu::PrivilegeMode::Supervisor, - memory.clone(), - &mut regs, - &mut meter, - ); - - assert_ne!(ptr, 0); - assert_eq!(ptr as usize % align, 0); - } -} - -#[test] -fn test_invalid_alignment() { - let memory: Memory = Rc::new(Sv32Memory::new(8192, PAGE_SIZE)); - let state = Rc::new(RefCell::new(State::new())); - let mut syscall_handler = DefaultSyscallHandler::new(state.clone()); - let mut regs = [0u32; 32]; - let mut meter = NoopMeter::default(); - - // Test invalid alignments (not powers of 2) - let invalid_alignments = [0, 3, 5, 6, 7, 9]; - - for &align in &invalid_alignments { - let args = [100, align as u32, 0, 0, 0, 0]; - let (ptr, _) = syscall_handler.handle_syscall( - SYSCALL_ALLOC, - args, - vm::cpu::PrivilegeMode::Supervisor, - memory.clone(), - &mut regs, - &mut meter, - ); - - assert_eq!(ptr, 0); - } -} diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 7c1d57a..972bc14 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -11,7 +11,7 @@ default = [] guest_kernel = [] [dependencies] -program = { path = "../program" } +program = { path = "../program", features = ["kernel"] } types = { path = "../types" } state = { path = "../state" } diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 8b80cee..8bae84e 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -2,6 +2,11 @@ //! are now dispatched from the kernel trap handler. Implementations will //! land here; for now they panic to make missing pieces explicit. use program::{log, logf}; +use program::syscalls::{ + SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, + SYSCALL_FIRE_EVENT, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, + SYSCALL_TRANSFER, +}; pub mod alloc; pub mod call_program; @@ -16,18 +21,6 @@ use panic::sys_panic; use storage::{sys_storage_get, sys_storage_set}; pub(crate) use panic::sys_panic_with_message; -pub const SYSCALL_STORAGE_GET: u32 = 1; -pub const SYSCALL_STORAGE_SET: u32 = 2; -pub const SYSCALL_PANIC: u32 = 3; -pub const SYSCALL_LOG: u32 = 100; -pub const SYSCALL_CALL_PROGRAM: u32 = 5; -pub const SYSCALL_FIRE_EVENT: u32 = 6; -pub const SYSCALL_ALLOC: u32 = 7; -pub const SYSCALL_DEALLOC: u32 = 8; -pub const SYSCALL_TRANSFER: u32 = 9; -pub const SYSCALL_BALANCE: u32 = 10; -pub const SYSCALL_BRK: u32 = 214; - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CallerMode { User, @@ -39,12 +32,15 @@ pub struct SyscallContext<'a> { pub caller_mode: CallerMode, } +pub trait SyscallHandler: core::fmt::Debug { + fn handle_syscall(&mut self, call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32; +} + pub fn dispatch_syscall(call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<'_>) -> u32 { match call_id { SYSCALL_STORAGE_GET => sys_storage_get(args), SYSCALL_STORAGE_SET => sys_storage_set(args), SYSCALL_PANIC => sys_panic(args), - SYSCALL_LOG => sys_log(args, ctx.caller_mode), SYSCALL_CALL_PROGRAM => sys_call_program(args, ctx), SYSCALL_FIRE_EVENT => sys_fire_event(args), SYSCALL_ALLOC => sys_alloc(args), @@ -59,16 +55,6 @@ pub fn dispatch_syscall(call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<' } } -fn sys_log(_args: [u32; 6], caller_mode: CallerMode) -> u32 { - if caller_mode == CallerMode::Supervisor { - log!("kernel: sys_log: need implementation"); - } else { - log!("guest: sys_log: need implementation"); - } - 0 -} - - fn sys_transfer(_args: [u32; 6]) -> u32 { log!("sys_transfer: need implementation"); 0 diff --git a/crates/program/Cargo.toml b/crates/program/Cargo.toml index 43ba48c..88ad04b 100644 --- a/crates/program/Cargo.toml +++ b/crates/program/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [features] guest = [] +kernel = [] [dependencies] types = { path = "../types" } diff --git a/crates/program/src/lib.rs b/crates/program/src/lib.rs index b04cfe7..e16de65 100644 --- a/crates/program/src/lib.rs +++ b/crates/program/src/lib.rs @@ -20,6 +20,10 @@ pub mod transfer; pub use transfer::transfer; pub use transfer::balance; +// Syscall IDs +pub mod syscalls; +pub use syscalls::*; + // StorageMap pub mod storage_map; pub use storage_map::StorageMap; @@ -32,6 +36,14 @@ pub use event::*; // Logging macros pub mod log; pub use log::BufferWriter; +pub use log::CONSOLE_WRITE_ID; + +#[cfg(feature = "kernel")] +pub const LOG_PREFIX: &str = "🛡️ Kernel: "; +#[cfg(all(not(feature = "kernel"), feature = "guest"))] +pub const LOG_PREFIX: &str = "📜 Guest: "; +#[cfg(all(not(feature = "kernel"), not(feature = "guest")))] +pub const LOG_PREFIX: &str = ""; // Data parser pub mod parser; diff --git a/crates/program/src/log.rs b/crates/program/src/log.rs index 44ce687..a560364 100644 --- a/crates/program/src/log.rs +++ b/crates/program/src/log.rs @@ -1,15 +1,18 @@ +pub const CONSOLE_WRITE_ID: u32 = 1000; + #[macro_export] macro_rules! logf_syscall { ($fmt_ptr:expr, $fmt_len:expr, $args_ptr:expr, $args_len:expr) => {{ #[cfg(target_arch = "riscv32")] unsafe { core::arch::asm!( - "li a7, 100", // syscall_log + "li a7, {console_write}", "ecall", in("a1") $fmt_ptr, in("a2") $fmt_len, in("a3") $args_ptr, in("a4") $args_len, + console_write = const $crate::CONSOLE_WRITE_ID, clobber_abi("C"), ); } @@ -25,6 +28,9 @@ macro_rules! logf { ($fmt:expr) => {{ // Handle both string literals and byte strings let fmt_bytes: &[u8] = $crate::as_bytes!($fmt); + let prefix_bytes = $crate::LOG_PREFIX.as_bytes(); + let mut fmt_buf = [0u8; 256]; + let fmt_bytes = $crate::concat_str!(fmt_buf, prefix_bytes, fmt_bytes); let fmt_ptr = fmt_bytes.as_ptr(); let fmt_len = fmt_bytes.len(); $crate::logf_syscall!(fmt_ptr, fmt_len, 0 as *const u32, 0usize); @@ -84,6 +90,9 @@ macro_rules! logf { )+ let fmt_bytes: &[u8] = $crate::as_bytes!($fmt); + let prefix_bytes = $crate::LOG_PREFIX.as_bytes(); + let mut fmt_buf = [0u8; 256]; + let fmt_bytes = $crate::concat_str!(fmt_buf, prefix_bytes, fmt_bytes); let fmt_ptr = fmt_bytes.as_ptr(); let fmt_len = fmt_bytes.len(); let args_ptr = args_buf.as_ptr(); diff --git a/crates/program/src/syscalls.rs b/crates/program/src/syscalls.rs new file mode 100644 index 0000000..73e6167 --- /dev/null +++ b/crates/program/src/syscalls.rs @@ -0,0 +1,11 @@ +/// System call IDs shared between the guest program ABI and the runtime. +pub const SYSCALL_STORAGE_GET: u32 = 1; +pub const SYSCALL_STORAGE_SET: u32 = 2; +pub const SYSCALL_PANIC: u32 = 3; +pub const SYSCALL_CALL_PROGRAM: u32 = 5; +pub const SYSCALL_FIRE_EVENT: u32 = 6; +pub const SYSCALL_ALLOC: u32 = 7; +pub const SYSCALL_DEALLOC: u32 = 8; +pub const SYSCALL_TRANSFER: u32 = 9; +pub const SYSCALL_BALANCE: u32 = 10; +pub const SYSCALL_BRK: u32 = 214; // brk(2): set program break (heap end) diff --git a/crates/vm/src/console.rs b/crates/vm/src/console.rs new file mode 100644 index 0000000..6e0a157 --- /dev/null +++ b/crates/vm/src/console.rs @@ -0,0 +1,229 @@ +use crate::cpu::PrivilegeMode; +use crate::memory::{Memory, VirtualAddress}; +use crate::metering::{MeterResult, Metering}; +use core::fmt::Write; +use std::cell::RefCell; +use std::rc::Rc; +use std::string::String; +use std::vec::Vec; + +pub const CONSOLE_WRITE_ID: u32 = 1000; + +enum Arg { + U32(u32), + F32(f32), + Char(char), + Str(String), + Bytes(Vec), +} + +pub fn console_write( + args: [u32; 6], + caller_mode: PrivilegeMode, + memory: Memory, + metering: &mut dyn Metering, + verbose_writer: &Option>>, +) -> u32 { + let [fmt_ptr, fmt_len, arg_ptr, arg_len, ..] = args; + let payload_len = fmt_len.saturating_add(arg_len) as usize; + if matches!( + metering.on_syscall_data(CONSOLE_WRITE_ID, payload_len), + MeterResult::Halt + ) { + panic!("Metering halted console write"); + } + let borrowed_memory = memory.as_ref(); + let (fmt_start, fmt_end) = va_range(fmt_ptr as usize, fmt_len as usize); + let fmt_slice = match borrowed_memory.mem_slice(fmt_start, fmt_end) { + Some(s) => s, + None => { + println!("invalid format string @ 0x{:08x}", fmt_ptr); + return 0; + } + }; + let fmt_bytes = fmt_slice.as_ref(); + let fmt = match core::str::from_utf8(fmt_bytes) { + Ok(s) => s, + Err(e) => { + println!("invalid UTF-8 in format string"); + println!("bytes: {:?}", fmt_bytes); + println!("error: {}", e); + return 0; + } + }; + let (args_start, args_end) = va_range(arg_ptr as usize, arg_len as usize); + let args_bytes_slice = borrowed_memory.mem_slice(args_start, args_end); + let args_bytes_holder; + let args_bytes: &[u8] = if let Some(slice) = args_bytes_slice { + args_bytes_holder = slice; + args_bytes_holder.as_ref() + } else { + b"" + }; + let raw_args: Vec = args_bytes + .chunks_exact(4) + .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(); + let mut args: Vec = Vec::new(); + let mut raw_iter = raw_args.into_iter(); + let mut chars = fmt.chars().peekable(); + while let Some(c) = chars.next() { + if c != '%' { + continue; + } + let spec: char = chars.next().unwrap_or('%'); + let mut next = || raw_iter.next().unwrap_or(0); + match spec { + 'd' | 'u' | 'x' => args.push(Arg::U32(next())), + 'f' => args.push(Arg::F32(f32::from_bits(next()))), + 'c' => args.push(Arg::Char(char::from_u32(next()).unwrap_or('?'))), + 's' => { + let ptr = next() as usize; + let len = next() as usize; + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + let s_ptr = core::str::from_utf8(slice.as_ref()); + args.push(match s_ptr { + Ok(s) => Arg::Str(s.to_string()), + Err(_) => Arg::Str("".to_string()), + }); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'b' => { + let ptr = next() as usize; + let len = next() as usize; + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'a' => { + let ptr = next() as usize; + let len = next() as usize; + let byte_len = len * 4; + let (start, end) = va_range(ptr, byte_len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + 'A' => { + let ptr = next() as usize; + let len = next() as usize; + let (start, end) = va_range(ptr, len); + match borrowed_memory.mem_slice(start, end) { + Some(slice) => { + args.push(Arg::Bytes(slice.to_vec())); + } + None => { + args.push(Arg::Str("".to_string())); + } + } + } + _ => args.push(Arg::Str("".to_string())), + } + } + let mut output = String::new(); + let mut args_iter = args.iter(); + let mut fmt_chars = fmt.chars().peekable(); + while let Some(c) = fmt_chars.next() { + if c == '%' { + match fmt_chars.next() { + Some('d') | Some('u') => match args_iter.next() { + Some(Arg::U32(v)) => output.push_str(&format!("{}", *v as i32)), + _ => output.push_str(""), + }, + Some('x') => match args_iter.next() { + Some(Arg::U32(v)) => output.push_str(&format!("{:08x}", v)), + _ => output.push_str(""), + }, + Some('f') => match args_iter.next() { + Some(Arg::F32(f)) => output.push_str(&format!("{}", f)), + _ => output.push_str(""), + }, + Some('c') => match args_iter.next() { + Some(Arg::Char(c)) => output.push(*c), + _ => output.push_str(""), + }, + Some('s') => match args_iter.next() { + Some(Arg::Str(s)) => output.push_str(s), + _ => output.push_str(""), + }, + Some('b') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + output.push('['); + for (i, byte) in b.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(&format!("0x{:02x}", byte)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('a') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + output.push('['); + for (i, chunk) in b.chunks_exact(4).enumerate() { + if i > 0 { + output.push_str(", "); + } + let val = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + output.push_str(&format!("{}", val)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('A') => match args_iter.next() { + Some(Arg::Bytes(b)) => { + output.push('['); + for (i, byte) in b.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + output.push_str(&format!("{}", byte)); + } + output.push(']'); + } + _ => output.push_str(""), + }, + Some('%') => output.push('%'), + Some(_) | None => output.push_str("<%?>"), + } + } else { + output.push(c); + } + } + let _ = caller_mode; + match verbose_writer { + Some(writer) => { + let _ = writeln!(writer.borrow_mut(), "{}", output); + } + None => { + println!("{}", output); + } + } + 0 +} + +fn va_range(ptr: usize, len: usize) -> (VirtualAddress, VirtualAddress) { + let start = VirtualAddress(ptr as u32); + let end = start.wrapping_add(len as u32); + (start, end) +} diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index 057cdad..b67e6c5 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -2,7 +2,6 @@ use crate::decoder::{decode_compressed, decode_full}; use crate::instruction::Instruction; use crate::memory::{Memory, VirtualAddress}; use crate::metering::{MemoryAccessKind, MeterResult, Metering, NoopMeter}; -use crate::sys_call::SyscallHandler; use core::cell::RefCell; use core::fmt::Write; use std::collections::HashMap; @@ -77,8 +76,6 @@ pub struct CPU { /// EDUCATIONAL: This helps students understand what the CPU is doing /// by printing each instruction as it executes pub verbose: bool, - pub syscall_handler: Box, - /// Reservation address for LR/SC atomic operations /// EDUCATIONAL: This implements the Load-Reserved/Store-Conditional /// mechanism for atomic memory operations in RISC-V @@ -124,20 +121,18 @@ impl CPU { /// - PC starts at 0 (first instruction) /// - All registers start at 0 (except x0 which is always 0) /// - Verbose logging is disabled by default - pub fn new(syscall_handler: Box) -> Self { - Self::with_metering(syscall_handler, Box::new(NoopMeter::default())) + pub fn new() -> Self { + Self::with_metering(Box::new(NoopMeter::default())) } /// Creates a new CPU instance with a custom metering implementation. pub fn with_metering( - syscall_handler: Box, metering: Box, ) -> Self { Self { pc: 0, regs: [0; 32], verbose: false, - syscall_handler, reservation_addr: None, verbose_writer: None, metering, diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index de2adba..11580de 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,5 +1,5 @@ use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP, CSR_SEPC, SCAUSE_BREAKPOINT}; -use crate::sys_call::SYSCALL_LOG; +use crate::console::{console_write, CONSOLE_WRITE_ID}; use crate::memory::VirtualAddress; use crate::instruction::CsrOp; use crate::registers::Register; @@ -791,30 +791,32 @@ impl CPU { Some(v) => v, None => return false, }; + if call_id == CONSOLE_WRITE_ID { + let result = console_write( + args, + self.priv_mode, + memory, + self.metering.as_mut(), + &self.verbose_writer, + ); + if !self.write_reg(Register::A0 as usize, result) { + return false; + } + return true; + } if self.has_trap_vector() { - // Bypass trap for logging syscalls so they execute directly. - if call_id != SYSCALL_LOG { - if !self.trap_to_vector(self.ecall_cause(), 0, Some(call_id)) { - panic!( - "trap_to_vector returned false for ecall id={} pc=0x{:08x}", - call_id, self.pc - ); - } - return true; + if !self.trap_to_vector(self.ecall_cause(), 0, Some(call_id)) { + panic!( + "trap_to_vector returned false for ecall id={} pc=0x{:08x}", + call_id, self.pc + ); } + return true; } - let (result, cont) = self.syscall_handler.handle_syscall( - call_id, - args, - self.priv_mode, - memory, - &mut self.regs, - self.metering.as_mut(), + panic!( + "ecall without trap vector for id={} pc=0x{:08x}", + call_id, self.pc ); - if !self.write_reg(Register::A0 as usize, result) { - return false; - } - return cont; } Instruction::Csr { rd, diff --git a/crates/vm/src/lib.rs b/crates/vm/src/lib.rs index 1723d9c..0df3e06 100644 --- a/crates/vm/src/lib.rs +++ b/crates/vm/src/lib.rs @@ -1,4 +1,5 @@ pub mod cpu; +pub mod console; pub mod decoder; pub mod instruction; pub mod isa; @@ -6,5 +7,4 @@ pub mod isa_compressed; pub mod memory; pub mod metering; pub mod registers; -pub mod sys_call; pub mod vm; diff --git a/crates/vm/src/sys_call.rs b/crates/vm/src/sys_call.rs deleted file mode 100644 index c75cc8e..0000000 --- a/crates/vm/src/sys_call.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::cpu::PrivilegeMode; -use crate::memory::Memory; -use crate::metering::Metering; -use core::any::Any; - -/// System call IDs for the VM. -pub const SYSCALL_STORAGE_GET: u32 = 1; -pub const SYSCALL_STORAGE_SET: u32 = 2; -pub const SYSCALL_PANIC: u32 = 3; -pub const SYSCALL_LOG: u32 = 100; -pub const SYSCALL_CALL_PROGRAM: u32 = 5; -pub const SYSCALL_FIRE_EVENT: u32 = 6; -pub const SYSCALL_ALLOC: u32 = 7; -pub const SYSCALL_DEALLOC: u32 = 8; -pub const SYSCALL_TRANSFER: u32 = 9; -pub const SYSCALL_BALANCE: u32 = 10; -pub const SYSCALL_BRK: u32 = 214; // brk(2): set program break (heap end) - -/// Trait implemented by syscall handlers consumed by the VM. -pub trait SyscallHandler: std::fmt::Debug { - fn handle_syscall( - &mut self, - call_id: u32, - args: [u32; 6], - caller_mode: PrivilegeMode, - memory: Memory, - regs: &mut [u32; 32], - metering: &mut dyn Metering, - ) -> (u32, bool); - fn as_any(&self) -> &dyn Any; -} diff --git a/crates/vm/src/vm.rs b/crates/vm/src/vm.rs index 7a9a1fe..f1c6cea 100644 --- a/crates/vm/src/vm.rs +++ b/crates/vm/src/vm.rs @@ -2,7 +2,6 @@ use crate::cpu::CPU; use crate::memory::{API, Memory}; use crate::metering::Metering; use crate::registers::Register; -use crate::sys_call::SyscallHandler; use std::rc::Rc; /// Represents a complete RISC-V virtual machine. @@ -30,18 +29,9 @@ pub struct VM { } impl VM { - /// Creates a new virtual machine with the specified memory and syscall handler. - pub fn new( - memory: Memory, - syscall_handler: Box, - ) -> Self { - Self::new_with_syscall_handler(memory, syscall_handler) - } - - /// Creates a new virtual machine with a custom syscall handler. - /// This is useful for testing or custom environments. - pub fn new_with_syscall_handler(memory: Memory, syscall_handler: Box) -> Self { - let mut cpu = CPU::new(syscall_handler); + /// Creates a new virtual machine with the specified memory. + pub fn new(memory: Memory) -> Self { + let mut cpu = CPU::new(); cpu.regs[Register::Sp as usize] = memory.stack_top().as_u32(); let satp = memory.satp(); cpu.set_satp(&memory, satp); From bd4169134b0c8b92614cb7720a66d044b4dc7b56 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 31 Dec 2025 15:00:25 +0200 Subject: [PATCH 53/70] rename program crate to clibc Rename the smart contract runtime crate to clibc (Chain Libc) and move it into crates/clibc. Update workspace members, crate dependencies, and Rust imports/macros across kernel, bootloader, examples, and compiler ABI generation. Refresh docs to reflect the new name and add a clibc README covering modules, macros, and usage. --- Cargo.lock | 22 +++++----- Cargo.toml | 2 +- README.md | 2 +- crates/bootloader/Cargo.toml | 4 +- crates/bootloader/README.md | 8 ++-- crates/{program => clibc}/Cargo.toml | 2 +- crates/clibc/README.md | 42 ++++++++++++++++++++ crates/{program => clibc}/src/allocator.rs | 0 crates/{program => clibc}/src/call.rs | 0 crates/{program => clibc}/src/entrypoint.rs | 2 +- crates/{program => clibc}/src/event.rs | 0 crates/{program => clibc}/src/integers.rs | 0 crates/{program => clibc}/src/lib.rs | 0 crates/{program => clibc}/src/log.rs | 0 crates/{program => clibc}/src/panic.rs | 0 crates/{program => clibc}/src/parser.rs | 0 crates/{program => clibc}/src/router.rs | 0 crates/{program => clibc}/src/storage.rs | 0 crates/{program => clibc}/src/storage_map.rs | 0 crates/{program => clibc}/src/syscalls.rs | 0 crates/{program => clibc}/src/transfer.rs | 0 crates/{program => clibc}/tests/router.rs | 2 +- crates/compiler/src/abi_codegen.rs | 6 +-- crates/examples/Cargo.toml | 2 +- crates/examples/src/allocator_demo.rs | 2 +- crates/examples/src/call_program.rs | 8 ++-- crates/examples/src/dex.rs | 4 +- crates/examples/src/ecdsa_verify.rs | 4 +- crates/examples/src/erc20.rs | 4 +- crates/examples/src/lib_import.rs | 6 +-- crates/examples/src/logging.rs | 6 +-- crates/examples/src/multi_func.rs | 8 ++-- crates/examples/src/native_transfer.rs | 14 +++---- crates/examples/src/simple.rs | 6 +-- crates/examples/src/storage.rs | 8 ++-- crates/kernel/Cargo.toml | 2 +- crates/kernel/src/bundle/create_account.rs | 4 +- crates/kernel/src/bundle/mod.rs | 2 +- crates/kernel/src/bundle/program_call.rs | 4 +- crates/kernel/src/bundle/result.rs | 2 +- crates/kernel/src/init.rs | 2 +- crates/kernel/src/lib.rs | 6 +-- crates/kernel/src/main.rs | 2 +- crates/kernel/src/syscall/alloc.rs | 2 +- crates/kernel/src/syscall/call_program.rs | 2 +- crates/kernel/src/syscall/fire_event.rs | 2 +- crates/kernel/src/syscall/mod.rs | 4 +- crates/kernel/src/syscall/panic.rs | 2 +- crates/kernel/src/syscall/storage.rs | 2 +- crates/kernel/src/task/prep.rs | 2 +- crates/kernel/src/task/run.rs | 2 +- crates/kernel/src/trap/mod.rs | 2 +- crates/kernel/src/user_program.rs | 2 +- 53 files changed, 126 insertions(+), 84 deletions(-) rename crates/{program => clibc}/Cargo.toml (91%) create mode 100644 crates/clibc/README.md rename crates/{program => clibc}/src/allocator.rs (100%) rename crates/{program => clibc}/src/call.rs (100%) rename crates/{program => clibc}/src/entrypoint.rs (99%) rename crates/{program => clibc}/src/event.rs (100%) rename crates/{program => clibc}/src/integers.rs (100%) rename crates/{program => clibc}/src/lib.rs (100%) rename crates/{program => clibc}/src/log.rs (100%) rename crates/{program => clibc}/src/panic.rs (100%) rename crates/{program => clibc}/src/parser.rs (100%) rename crates/{program => clibc}/src/router.rs (100%) rename crates/{program => clibc}/src/storage.rs (100%) rename crates/{program => clibc}/src/storage_map.rs (100%) rename crates/{program => clibc}/src/syscalls.rs (100%) rename crates/{program => clibc}/src/transfer.rs (100%) rename crates/{program => clibc}/tests/router.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 5b20b64..c0f8997 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,9 +27,9 @@ dependencies = [ name = "bootloader" version = "0.1.0" dependencies = [ + "clibc", "compiler", "goblin", - "program", "state", "types", "vm", @@ -41,6 +41,14 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "clibc" +version = "0.1.0" +dependencies = [ + "state", + "types", +] + [[package]] name = "compiler" version = "0.1.0" @@ -145,10 +153,10 @@ name = "examples" version = "0.1.0" dependencies = [ "bootloader", + "clibc", "compiler", "k256", "once_cell", - "program", "serde_json", "sha2", "state", @@ -235,7 +243,7 @@ dependencies = [ name = "kernel" version = "0.1.0" dependencies = [ - "program", + "clibc", "state", "types", ] @@ -289,14 +297,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "program" -version = "0.1.0" -dependencies = [ - "state", - "types", -] - [[package]] name = "quote" version = "1.0.40" diff --git a/Cargo.toml b/Cargo.toml index 3f21bc6..58ee6f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "crates/examples", "crates/bootloader", "crates/kernel", - "crates/program", + "crates/clibc", "crates/state", "crates/storage", "crates/types", diff --git a/README.md b/README.md index a6a8bca..f36fe76 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ rust-vm/ │ │ ├── storage.rs # Storage system examples │ │ ├── multi_func.rs # Multi-function contract │ │ └── call_program.rs # Cross-program calls -│ ├── program/ # Smart contract runtime library +│ ├── clibc/ # Chain Libc smart contract runtime library │ ├── state/ # Blockchain state management │ ├── storage/ # Persistent storage system │ ├── types/ # Common types and data structures diff --git a/crates/bootloader/Cargo.toml b/crates/bootloader/Cargo.toml index ad9c6bc..8cedb4d 100644 --- a/crates/bootloader/Cargo.toml +++ b/crates/bootloader/Cargo.toml @@ -15,7 +15,7 @@ vm = { path = "../vm" } state = { path = "../state" } goblin = "0.10" compiler = { path = "../compiler" } -program = { path = "../program" } +clibc = { path = "../clibc" } [target.'cfg(target_arch = "riscv32")'.dependencies] -program = { path = "../program", default-features = false } +clibc = { path = "../clibc", default-features = false } diff --git a/crates/bootloader/README.md b/crates/bootloader/README.md index c154734..b6eb703 100644 --- a/crates/bootloader/README.md +++ b/crates/bootloader/README.md @@ -13,7 +13,7 @@ Alon's OS (OS) is a minimal operating system purpose-built for deterministic, bl 1. **Bootloader**: First-stage loader that verifies the kernel and `liba` images, measures them, and passes a concise boot manifest to the kernel. Runs in a restricted environment with no dynamic allocation. 2. **Kernel**: Manages memory layout, page tables, capabilities, and the syscall surface. Provides deterministic scheduling and ties block context (slot, leader, parent state root) to every execution. 3. **Execution Runtime**: The blockchain-aware executor that will supersede `avm`. It coordinates transaction/block execution, drives the VM, and emits receipts and event logs. -4. **liba (Standard Library)**: Successor of the `program` crate, offering safe wrappers over syscalls (storage, logs, crypto, messaging), ABI helpers, and contract-to-contract call utilities. +4. **liba (Standard Library)**: Successor of the `clibc` crate, offering safe wrappers over syscalls (storage, logs, crypto, messaging), ABI helpers, and contract-to-contract call utilities. 5. **Tooling**: Compiler and host utilities reused from the existing workspace to build and package aOS images and applications. ## Component Details @@ -34,7 +34,7 @@ Alon's OS (OS) is a minimal operating system purpose-built for deterministic, bl - Provides hooks for precompiles and deterministic host functions. ### liba (Application Standard Library) -- Derived from the existing `program` crate and tailored to aOS. +- Derived from the existing `clibc` crate and tailored to aOS. - Offers ABI types, context helpers, and safe wrappers around syscalls exposed by the kernel. - Ships default modules for storage, logging/events, cross-program calls, and crypto utilities. @@ -47,12 +47,12 @@ Alon's OS (OS) is a minimal operating system purpose-built for deterministic, bl ## Relationship to Existing Workspace - `os` replaces the `avm` crate as the orchestrator/runtime. -- `program` is internalized as `liba` inside this crate (module structure will mirror the current APIs). +- `clibc` is internalized as `liba` inside this crate (module structure will mirror the current APIs). - `vm`, `state`, `storage`, `types`, and `compiler` remain the core building blocks for CPU execution, state transitions, persistence, shared types, and toolchain support. ## Roadmap (Initial Steps) - Define kernel <-> runtime <-> `liba` interfaces and shared types. -- Port `program` into `liba` and expose it as the supported app-facing API. +- Port `clibc` into `liba` and expose it as the supported app-facing API. - Migrate `avm` responsibilities into the aOS runtime and deprecate `avm`. - Add a boot manifest format and minimal bootloader stubs to validate and launch the kernel. - Document syscall semantics and determinism guarantees for contract authors. diff --git a/crates/program/Cargo.toml b/crates/clibc/Cargo.toml similarity index 91% rename from crates/program/Cargo.toml rename to crates/clibc/Cargo.toml index 88ad04b..7206893 100644 --- a/crates/program/Cargo.toml +++ b/crates/clibc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "program" +name = "clibc" version = "0.1.0" edition = "2024" diff --git a/crates/clibc/README.md b/crates/clibc/README.md new file mode 100644 index 0000000..a24ba41 --- /dev/null +++ b/crates/clibc/README.md @@ -0,0 +1,42 @@ +# clibc (Chain Libc) + +`clibc` is the smart contract runtime library for the Rust VM. It provides the +guest-facing API surface for syscalls, ABI helpers, storage primitives, logging, +and cross-program calls. The crate is `no_std` and built to run inside the VM. + +## Features +- `guest`: APIs intended for contract code running inside the VM. +- `kernel`: helpers used by the kernel/runtime side. + +## Module Overview +- `allocator`: VM-backed global allocator (enabled for RISC-V guest builds). +- `call`: cross-program call helper (`call`). +- `entrypoint`: `entrypoint!` macro for defining contract entry functions. +- `event`: `event!` definitions plus `fire_event!` dispatch. +- `integers`: simple integer readers (e.g., `read_u32`). +- `log`: logging macros (`log!`, `logf!`, `concat!`, `concat_str!`) and + `BufferWriter`. +- `panic`: `vm_panic` helper and guest panic handler. +- `parser`: `DataParser` and `HexCodec` utilities, plus `hex_address!` macro. +- `router`: `decode_calls`, `route`, and `FuncCall` for ABI routing. +- `storage`: `persist_struct!` macro and `Persistent` helpers. +- `storage_map`: `StorageMap`, `StorageKey`, and `Map!` macro for typed domains. +- `syscalls`: shared syscall IDs (storage, events, allocation, transfer). +- `transfer`: `transfer`, `balance`, and convenience macros. + +## Macros and Helpers +- `entrypoint!`: declare a contract entry function with a consistent ABI. +- `persist_struct!`: generate storage-backed struct load/store helpers. +- `Map!`: declare a typed storage map domain with get/set helpers. +- `event!` and `fire_event!`: define events and emit them via syscall. +- `log!`/`logf!`: basic logging and formatted logging. +- `transfer!`/`balance!`: concise wrappers for token transfer and balance. +- `hex_address!`: compile-time address parsing helper. +- `require`: guard helper that aborts execution with `vm_panic` on failure. + +## Usage +Add the crate to your workspace and import it as `clibc`: + +```rust +use clibc::{entrypoint, log, logf}; +``` diff --git a/crates/program/src/allocator.rs b/crates/clibc/src/allocator.rs similarity index 100% rename from crates/program/src/allocator.rs rename to crates/clibc/src/allocator.rs diff --git a/crates/program/src/call.rs b/crates/clibc/src/call.rs similarity index 100% rename from crates/program/src/call.rs rename to crates/clibc/src/call.rs diff --git a/crates/program/src/entrypoint.rs b/crates/clibc/src/entrypoint.rs similarity index 99% rename from crates/program/src/entrypoint.rs rename to crates/clibc/src/entrypoint.rs index 6fc79a2..b96658b 100644 --- a/crates/program/src/entrypoint.rs +++ b/crates/clibc/src/entrypoint.rs @@ -11,7 +11,7 @@ /// /// USAGE: Call this macro with the name of your main contract function: /// ```ignore -/// use program::entrypoint; +/// use clibc::entrypoint; /// entrypoint!(my_contract_function); /// ``` /// diff --git a/crates/program/src/event.rs b/crates/clibc/src/event.rs similarity index 100% rename from crates/program/src/event.rs rename to crates/clibc/src/event.rs diff --git a/crates/program/src/integers.rs b/crates/clibc/src/integers.rs similarity index 100% rename from crates/program/src/integers.rs rename to crates/clibc/src/integers.rs diff --git a/crates/program/src/lib.rs b/crates/clibc/src/lib.rs similarity index 100% rename from crates/program/src/lib.rs rename to crates/clibc/src/lib.rs diff --git a/crates/program/src/log.rs b/crates/clibc/src/log.rs similarity index 100% rename from crates/program/src/log.rs rename to crates/clibc/src/log.rs diff --git a/crates/program/src/panic.rs b/crates/clibc/src/panic.rs similarity index 100% rename from crates/program/src/panic.rs rename to crates/clibc/src/panic.rs diff --git a/crates/program/src/parser.rs b/crates/clibc/src/parser.rs similarity index 100% rename from crates/program/src/parser.rs rename to crates/clibc/src/parser.rs diff --git a/crates/program/src/router.rs b/crates/clibc/src/router.rs similarity index 100% rename from crates/program/src/router.rs rename to crates/clibc/src/router.rs diff --git a/crates/program/src/storage.rs b/crates/clibc/src/storage.rs similarity index 100% rename from crates/program/src/storage.rs rename to crates/clibc/src/storage.rs diff --git a/crates/program/src/storage_map.rs b/crates/clibc/src/storage_map.rs similarity index 100% rename from crates/program/src/storage_map.rs rename to crates/clibc/src/storage_map.rs diff --git a/crates/program/src/syscalls.rs b/crates/clibc/src/syscalls.rs similarity index 100% rename from crates/program/src/syscalls.rs rename to crates/clibc/src/syscalls.rs diff --git a/crates/program/src/transfer.rs b/crates/clibc/src/transfer.rs similarity index 100% rename from crates/program/src/transfer.rs rename to crates/clibc/src/transfer.rs diff --git a/crates/program/tests/router.rs b/crates/clibc/tests/router.rs similarity index 97% rename from crates/program/tests/router.rs rename to crates/clibc/tests/router.rs index fa00842..5f9a5a3 100644 --- a/crates/program/tests/router.rs +++ b/crates/clibc/tests/router.rs @@ -1,4 +1,4 @@ -use program::router::{decode_calls, route, FuncCall}; +use clibc::router::{decode_calls, route, FuncCall}; use types::{Result, Address}; #[test] diff --git a/crates/compiler/src/abi_codegen.rs b/crates/compiler/src/abi_codegen.rs index d7c0c3f..62725ba 100644 --- a/crates/compiler/src/abi_codegen.rs +++ b/crates/compiler/src/abi_codegen.rs @@ -27,9 +27,9 @@ impl AbiCodeGenerator { // Don't add imports - assume they're in the parent file code.push_str("// Note: This code assumes the following imports in the parent file:\n"); - code.push_str("// use program::types::address::Address;\n"); - code.push_str("// use program::types::result::Result;\n"); - code.push_str("// use program::call::call;\n\n"); + code.push_str("// use clibc::types::address::Address;\n"); + code.push_str("// use clibc::types::result::Result;\n"); + code.push_str("// use clibc::call::call;\n\n"); // Generate contract struct code.push_str(&format!("/// Client for interacting with {} contract\n", self.contract_name)); diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 15c5539..3d2011a 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -program = { path = "../program", features = ["guest"] } +clibc = { path = "../clibc", features = ["guest"] } sha2 = { version = "0.10", default-features = false } k256 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdsa", "alloc"] } diff --git a/crates/examples/src/allocator_demo.rs b/crates/examples/src/allocator_demo.rs index 9a2ba28..78ab601 100644 --- a/crates/examples/src/allocator_demo.rs +++ b/crates/examples/src/allocator_demo.rs @@ -3,7 +3,7 @@ extern crate alloc; -use program::{ +use clibc::{ DataParser, entrypoint, require, types::address::Address, types::result::Result, vm_panic, }; diff --git a/crates/examples/src/call_program.rs b/crates/examples/src/call_program.rs index 7b08dd0..9dfb935 100644 --- a/crates/examples/src/call_program.rs +++ b/crates/examples/src/call_program.rs @@ -1,11 +1,11 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::call::call; -use program::types::address::Address; -use program::{DataParser, entrypoint, require, types::result::Result, vm_panic}; +use clibc::call::call; +use clibc::types::address::Address; +use clibc::{DataParser, entrypoint, require, types::result::Result, vm_panic}; // Include the auto-generated ABI client code for simple program include!("../bin/simple_abi.rs"); diff --git a/crates/examples/src/dex.rs b/crates/examples/src/dex.rs index 27dfa85..dcc06bd 100644 --- a/crates/examples/src/dex.rs +++ b/crates/examples/src/dex.rs @@ -1,9 +1,9 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::{ +use clibc::{ DataParser, Map, call::call, entrypoint, event, fire_event, hex_address, persist_struct, require, transfer, diff --git a/crates/examples/src/ecdsa_verify.rs b/crates/examples/src/ecdsa_verify.rs index 47942b1..00b044e 100644 --- a/crates/examples/src/ecdsa_verify.rs +++ b/crates/examples/src/ecdsa_verify.rs @@ -1,9 +1,9 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; use k256::ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier}; -use program::{ +use clibc::{ DataParser, HexCodec, entrypoint, log, logf, require, types::address::Address, types::result::Result, vm_panic, }; diff --git a/crates/examples/src/erc20.rs b/crates/examples/src/erc20.rs index 9d00a44..5bf72f7 100644 --- a/crates/examples/src/erc20.rs +++ b/crates/examples/src/erc20.rs @@ -1,8 +1,8 @@ #![no_std] #![no_main] -extern crate program; -use program::{ +extern crate clibc; +use clibc::{ DataParser, Map, entrypoint, event, fire_event, log, logf, persist_struct, require, router::route, types::{address::Address, o::O, result::Result}, diff --git a/crates/examples/src/lib_import.rs b/crates/examples/src/lib_import.rs index c75c654..1a9add2 100644 --- a/crates/examples/src/lib_import.rs +++ b/crates/examples/src/lib_import.rs @@ -1,9 +1,9 @@ #![no_std] #![no_main] -extern crate program; -use program::types::address::Address; -use program::{entrypoint, require, types::result::Result}; +extern crate clibc; +use clibc::types::address::Address; +use clibc::{entrypoint, require, types::result::Result}; // Import the sha2 library for hashing use sha2::{Digest, Sha256}; diff --git a/crates/examples/src/logging.rs b/crates/examples/src/logging.rs index f47065b..7aced04 100644 --- a/crates/examples/src/logging.rs +++ b/crates/examples/src/logging.rs @@ -1,10 +1,10 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; use core::fmt; -use program::types::address::Address; -use program::{DataParser, concat_str, entrypoint, log, logf, types::result::Result}; +use clibc::types::address::Address; +use clibc::{DataParser, concat_str, entrypoint, log, logf, types::result::Result}; /// Comprehensive logging demonstration showing all format specifiers unsafe fn logging(program: Address, _caller: Address, data: &[u8]) -> Result { diff --git a/crates/examples/src/multi_func.rs b/crates/examples/src/multi_func.rs index 02cc61d..4d8fe4f 100644 --- a/crates/examples/src/multi_func.rs +++ b/crates/examples/src/multi_func.rs @@ -1,11 +1,11 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::router::route; -use program::types::address::Address; -use program::{DataParser, entrypoint, require, types::result::Result, vm_panic}; +use clibc::router::route; +use clibc::types::address::Address; +use clibc::{DataParser, entrypoint, require, types::result::Result, vm_panic}; /// Main entry point for the smart contract. /// diff --git a/crates/examples/src/native_transfer.rs b/crates/examples/src/native_transfer.rs index 844b589..d9bf350 100644 --- a/crates/examples/src/native_transfer.rs +++ b/crates/examples/src/native_transfer.rs @@ -1,11 +1,11 @@ #![no_std] #![no_main] -extern crate program; +extern crate clibc; -use program::types::address::Address; -use program::types::result::Result; -use program::{DataParser, entrypoint, require}; +use clibc::types::address::Address; +use clibc::types::result::Result; +use clibc::{DataParser, entrypoint, require}; /// Demonstrates transferring the native AM token from the caller to a target /// address using the VM's transfer syscall. The input payload is: @@ -21,9 +21,9 @@ fn transfer_entry(program: Address, _caller: Address, data: &[u8]) -> Result { let amount = parser.read_u64(); // Capture recipient balance before/after for return value - let _before = program::balance!(&to); - let ok = program::transfer!(&to, amount); - let after = program::balance!(&to); + let _before = clibc::balance!(&to); + let ok = clibc::transfer!(&to, amount); + let after = clibc::balance!(&to); // Encode success flag in data for easier assertions let mut result = Result::new(ok, if ok { 0 } else { 1 }); diff --git a/crates/examples/src/simple.rs b/crates/examples/src/simple.rs index 9a709b3..71685cc 100644 --- a/crates/examples/src/simple.rs +++ b/crates/examples/src/simple.rs @@ -1,9 +1,9 @@ #![no_std] #![no_main] -extern crate program; -use program::types::address::Address; -use program::{DataParser, entrypoint, require, types::result::Result}; +extern crate clibc; +use clibc::types::address::Address; +use clibc::{DataParser, entrypoint, require, types::result::Result}; /// Simple smart contract that compares two 32-bit integers. /// diff --git a/crates/examples/src/storage.rs b/crates/examples/src/storage.rs index a4a5def..53b62c2 100644 --- a/crates/examples/src/storage.rs +++ b/crates/examples/src/storage.rs @@ -1,10 +1,10 @@ #![no_std] #![no_main] -extern crate program; -use program::persist_struct; -use program::types::address::Address; -use program::{entrypoint, require, types::result::Result}; +extern crate clibc; +use clibc::persist_struct; +use clibc::types::address::Address; +use clibc::{entrypoint, require, types::result::Result}; // Struct 1: User profile persist_struct!(User { diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 972bc14..b13e8f6 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -11,7 +11,7 @@ default = [] guest_kernel = [] [dependencies] -program = { path = "../program", features = ["kernel"] } +clibc = { path = "../clibc", features = ["kernel"] } types = { path = "../types" } state = { path = "../state" } diff --git a/crates/kernel/src/bundle/create_account.rs b/crates/kernel/src/bundle/create_account.rs index b6bf9ff..420b5f4 100644 --- a/crates/kernel/src/bundle/create_account.rs +++ b/crates/kernel/src/bundle/create_account.rs @@ -1,7 +1,7 @@ use kernel::global::STATE; use kernel::Config; -use program::logf; -use program::parser::HexCodec; +use clibc::logf; +use clibc::parser::HexCodec; use state::State; use types::transaction::Transaction; diff --git a/crates/kernel/src/bundle/mod.rs b/crates/kernel/src/bundle/mod.rs index 92def24..8fd3ab2 100644 --- a/crates/kernel/src/bundle/mod.rs +++ b/crates/kernel/src/bundle/mod.rs @@ -3,7 +3,7 @@ use core::mem::forget; extern crate alloc; use alloc::vec::Vec; -use program::{log, logf}; +use clibc::{log, logf}; use types::transaction::{Transaction, TransactionBundle, TransactionType}; use types::{Result, TransactionReceipt}; diff --git a/crates/kernel/src/bundle/program_call.rs b/crates/kernel/src/bundle/program_call.rs index 2acea68..b059eb9 100644 --- a/crates/kernel/src/bundle/program_call.rs +++ b/crates/kernel/src/bundle/program_call.rs @@ -1,8 +1,8 @@ use kernel::{kernel_run_task, prep_program_task, PROGRAM_WINDOW_BYTES}; use kernel::global::TASKS; use kernel::user_program::with_program_image; -use program::{log, logf}; -use program::parser::HexCodec; +use clibc::{log, logf}; +use clibc::parser::HexCodec; use types::transaction::Transaction; pub(crate) fn program_call(tx: &Transaction, resume: extern "C" fn() -> !) { diff --git a/crates/kernel/src/bundle/result.rs b/crates/kernel/src/bundle/result.rs index 4e1589e..ebfed17 100644 --- a/crates/kernel/src/bundle/result.rs +++ b/crates/kernel/src/bundle/result.rs @@ -1,6 +1,6 @@ use kernel::Config; use kernel::global::{CURRENT_TX, LAST_COMPLETED_TASK, RECEIPTS, TASKS}; -use program::{log, logf}; +use clibc::{log, logf}; use types::{KernelResult, TransactionReceipt}; pub(crate) fn update_receipt_from_task() { diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index 108b3ce..0d36594 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -1,6 +1,6 @@ use core::{cmp, slice}; -use program::{log, logf}; +use clibc::{log, logf}; use state::State; use kernel::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index d9777b2..c5acc41 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -26,7 +26,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! { let mut buf = [0u8; 256]; let len = { - let mut writer = program::BufferWriter::new(&mut buf); + let mut writer = clibc::BufferWriter::new(&mut buf); if write!(&mut writer, "{}", info).is_ok() { writer.len() } else { @@ -34,9 +34,9 @@ fn panic(info: &core::panic::PanicInfo) -> ! { } }; if len == 0 { - program::log!("kernel panic"); + clibc::log!("kernel panic"); } else { - program::logf!("kernel panic: %s", buf.as_ptr() as u32, len as u32); + clibc::logf!("kernel panic: %s", buf.as_ptr() as u32, len as u32); } unsafe { core::arch::asm!("ebreak") }; loop {} diff --git a/crates/kernel/src/main.rs b/crates/kernel/src/main.rs index eeac708..b0b82c1 100644 --- a/crates/kernel/src/main.rs +++ b/crates/kernel/src/main.rs @@ -5,7 +5,7 @@ extern crate alloc; use core::slice; use kernel::BootInfo; -use program::{log, logf}; +use clibc::{log, logf}; mod init; mod bundle; diff --git a/crates/kernel/src/syscall/alloc.rs b/crates/kernel/src/syscall/alloc.rs index 4805466..9f2094f 100644 --- a/crates/kernel/src/syscall/alloc.rs +++ b/crates/kernel/src/syscall/alloc.rs @@ -1,4 +1,4 @@ -use program::{log, logf}; +use clibc::{log, logf}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; use crate::Task; diff --git a/crates/kernel/src/syscall/call_program.rs b/crates/kernel/src/syscall/call_program.rs index 7f020f8..f2e4539 100644 --- a/crates/kernel/src/syscall/call_program.rs +++ b/crates/kernel/src/syscall/call_program.rs @@ -1,4 +1,4 @@ -use program::logf; +use clibc::logf; use types::{Address, ADDRESS_LEN}; use crate::global::{CURRENT_TASK, TASKS}; diff --git a/crates/kernel/src/syscall/fire_event.rs b/crates/kernel/src/syscall/fire_event.rs index d8d0c33..d2a9c31 100644 --- a/crates/kernel/src/syscall/fire_event.rs +++ b/crates/kernel/src/syscall/fire_event.rs @@ -1,4 +1,4 @@ -use program::logf; +use clibc::logf; use crate::global::{CURRENT_TX, RECEIPTS}; use crate::syscall::storage::{current_root_ppn, read_user_bytes}; diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 8bae84e..7b244ce 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -1,8 +1,8 @@ //! Kernel-owned syscall stubs. These mirror the bootloader syscalls but //! are now dispatched from the kernel trap handler. Implementations will //! land here; for now they panic to make missing pieces explicit. -use program::{log, logf}; -use program::syscalls::{ +use clibc::{log, logf}; +use clibc::syscalls::{ SYSCALL_ALLOC, SYSCALL_BALANCE, SYSCALL_BRK, SYSCALL_CALL_PROGRAM, SYSCALL_DEALLOC, SYSCALL_FIRE_EVENT, SYSCALL_PANIC, SYSCALL_STORAGE_GET, SYSCALL_STORAGE_SET, SYSCALL_TRANSFER, diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs index 0e16591..63411c9 100644 --- a/crates/kernel/src/syscall/panic.rs +++ b/crates/kernel/src/syscall/panic.rs @@ -1,4 +1,4 @@ -use program::{log, logf}; +use clibc::{log, logf}; use types::{SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; use crate::global::{CURRENT_TASK, TASKS}; diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index 72f7035..4456acc 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -3,7 +3,7 @@ extern crate alloc; use alloc::{format, string::String, vec, vec::Vec}; use core::cmp; -use program::{log, logf}; +use clibc::{log, logf}; use types::{Address, ADDRESS_LEN, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 1529426..725f7a6 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,7 +1,7 @@ use crate::{AddressSpace, Config, Task}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; use crate::memory::page_allocator as mmu; -use program::{log, logf}; +use clibc::{log, logf}; use types::address::Address; use super::{ diff --git a/crates/kernel/src/task/run.rs b/crates/kernel/src/task/run.rs index 509f5aa..b6674db 100644 --- a/crates/kernel/src/task/run.rs +++ b/crates/kernel/src/task/run.rs @@ -1,6 +1,6 @@ use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; use crate::memory::page_allocator as mmu; -use program::logf; +use clibc::logf; use super::{REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, TRAMPOLINE_VA, TRAP_TRAMPOLINE_VA}; diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index 2235b5a..1c513f4 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -1,5 +1,5 @@ use core::arch::asm; -use program::{log, logf}; +use clibc::{log, logf}; use types::result::{Result as VmResult, RESULT_DATA_SIZE}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, LAST_COMPLETED_TASK, TASKS}; diff --git a/crates/kernel/src/user_program.rs b/crates/kernel/src/user_program.rs index e5c515d..aea05d5 100644 --- a/crates/kernel/src/user_program.rs +++ b/crates/kernel/src/user_program.rs @@ -1,7 +1,7 @@ extern crate alloc; use alloc::format; -use program::logf; +use clibc::logf; use state::State; use types::address::Address; From 13ec827506afe1d95b8529bb2e4f004628c884a9 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Wed, 31 Dec 2025 17:28:08 +0200 Subject: [PATCH 54/70] Implement native transfer/balance + state handoff - add shared State transfer helpers and bundle transfer processing - implement transfer/balance syscalls and move to balance module - add kernel result state pointer/len with safe encoding - update bootloader/test runner for state capture - keep state crate no_std and clean unused helpers --- Cargo.lock | 8 - crates/bootloader/src/bootloader.rs | 6 +- crates/bootloader/src/result.rs | 24 +- crates/examples/Cargo.toml | 2 +- crates/examples/tests/common/test_runner.rs | 62 ++++- crates/kernel/src/bundle/mod.rs | 6 + crates/kernel/src/bundle/result.rs | 25 +- crates/kernel/src/bundle/transfer.rs | 27 ++ crates/kernel/src/syscall/balance.rs | 92 +++++++ crates/kernel/src/syscall/mod.rs | 12 +- crates/kernel/src/task/mod.rs | 2 +- crates/state/Cargo.toml | 2 - crates/state/src/lib.rs | 4 +- crates/state/src/state.rs | 265 +++++++------------- crates/types/src/kernel_result.rs | 4 +- 15 files changed, 336 insertions(+), 205 deletions(-) create mode 100644 crates/kernel/src/bundle/transfer.rs create mode 100644 crates/kernel/src/syscall/balance.rs diff --git a/Cargo.lock b/Cargo.lock index c0f8997..8f4da61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,12 +206,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hmac" version = "0.12.1" @@ -429,8 +423,6 @@ dependencies = [ name = "state" version = "0.1.0" dependencies = [ - "hex", - "storage", "types", ] diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index bb0905b..7a9a626 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -6,7 +6,7 @@ use std::vec::Vec; use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; -use types::{boot::BootInfo, transaction::TransactionBundle, TransactionReceipt, SV32_DIRECT_MAP_BASE}; +use types::{boot::BootInfo, transaction::TransactionBundle, SV32_DIRECT_MAP_BASE}; use state::State; use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, VirtualAddress, PAGE_SIZE}; @@ -138,7 +138,7 @@ impl Bootloader { state: Rc>, verbose: bool, verbose_writer: Option>>, - ) -> Option> { + ) -> Option { let (entry_point, memory) = self.load_kernel(kernel_elf); let mut vm = VM::new(memory.clone()); vm.set_reg_u32(Register::Sp, KERNEL_STACK_TOP); @@ -153,7 +153,7 @@ impl Bootloader { self.place_state(&mut vm, &encoded_state); self.place_boot_info(&mut vm); vm.raw_run(); - crate::result::read_kernel_receipts(&memory) + crate::result::read_kernel_result(&memory) } fn place_bundle(&mut self, vm: &mut VM, bundle: &TransactionBundle) { diff --git a/crates/bootloader/src/result.rs b/crates/bootloader/src/result.rs index e483747..3ad14fa 100644 --- a/crates/bootloader/src/result.rs +++ b/crates/bootloader/src/result.rs @@ -2,9 +2,15 @@ use core::mem; use types::kernel_result::KERNEL_RESULT_ADDR; use types::{KernelResult, TransactionReceipt}; +use state::State; use vm::memory::{Memory as MmuRef, VirtualAddress}; -pub(crate) fn read_kernel_receipts(memory: &MmuRef) -> Option> { +pub struct KernelRunResult { + pub receipts: Vec, + pub state: Option, +} + +pub(crate) fn read_kernel_result(memory: &MmuRef) -> Option { let header_size = mem::size_of::() as u32; let header_end = KERNEL_RESULT_ADDR.checked_add(header_size)?; let header_slice = memory.mem_slice( @@ -17,6 +23,8 @@ pub(crate) fn read_kernel_receipts(memory: &MmuRef) -> Option Option Result<(), String> { let mut bootloader = Bootloader::new(self.vm_memory_size); - let state = Rc::new(RefCell::new(State::new())); + let state = Rc::new(RefCell::new(test_state())); // Write test case header writeln!( @@ -149,7 +150,7 @@ impl TestRunner { writeln!(self.writer.borrow_mut()).unwrap(); // Execute the whole bundle via the bootloader/kernel path. - let receipts = bootloader.execute_bundle( + let result = bootloader.execute_bundle( self.kernel_bytes.as_ref().ok_or_else(|| { "KERNEL_ELF not set or unreadable; bootloader path required".to_string() })?, @@ -163,14 +164,19 @@ impl TestRunner { }, ); - let receipts = match receipts { - Some(receipts) => receipts, + let result = match result { + Some(result) => result, None => { return Err("Bootloader returned no receipts".to_string()); } }; + if let Some(post_state) = result.state { + println!("\n=== State After Execution ==="); + print_state(&post_state); + } - let receipt = receipts + let receipt = result + .receipts .last() .ok_or_else(|| "No receipts returned from kernel".to_string())?; writeln!(self.writer.borrow_mut(), "\n=== Receipt ===").unwrap(); @@ -231,6 +237,52 @@ impl TestRunner { } } +fn print_state(state: &State) { + println!("--- State Dump ---"); + for (addr, acc) in &state.accounts { + println!(" Address: 0x{}", hex_encode(addr.0)); + println!(" - Balance: {}", acc.balance); + println!(" - Nonce: {}", acc.nonce); + println!(" - Is contract?: {}", acc.is_contract); + println!(" - Code size: {} bytes", acc.code.len()); + println!(" - Storage:"); + for (key, value) in &acc.storage { + let value_hex = hex_join(value); + println!( + " Key: {:<20} | Value ({} bytes): {}", + key, + value.len(), + value_hex + ); + } + println!(); + } + println!("--------------------"); +} + +fn hex_encode(bytes: [u8; 20]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(40); + for b in bytes { + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0f) as usize] as char); + } + out +} + +fn hex_join(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len().saturating_mul(3)); + for (idx, b) in bytes.iter().enumerate() { + if idx > 0 { + out.push(' '); + } + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0f) as usize] as char); + } + out +} + impl Default for TestRunner { fn default() -> Self { Self::with_writer(Rc::new(RefCell::new(ConsoleWriter))) diff --git a/crates/kernel/src/bundle/mod.rs b/crates/kernel/src/bundle/mod.rs index 8fd3ab2..fbb2a45 100644 --- a/crates/kernel/src/bundle/mod.rs +++ b/crates/kernel/src/bundle/mod.rs @@ -11,10 +11,12 @@ use kernel::global::{BUNDLE, CURRENT_TX, RECEIPTS}; mod create_account; mod program_call; +mod transfer; mod result; use self::create_account::create_account; use self::program_call::program_call; +use self::transfer::transfer; use self::result::{update_receipt_from_task, write_kernel_result}; pub(crate) fn decode_bundle(encoded_bundle: &[u8]) -> bool { @@ -88,6 +90,10 @@ fn execute_transaction(tx: &Transaction) -> bool { program_call(tx, resume_bundle); false } + TransactionType::Transfer => { + transfer(tx); + true + } _ => panic!("unsupported transaction type"), } } diff --git a/crates/kernel/src/bundle/result.rs b/crates/kernel/src/bundle/result.rs index ebfed17..711e832 100644 --- a/crates/kernel/src/bundle/result.rs +++ b/crates/kernel/src/bundle/result.rs @@ -1,5 +1,6 @@ use kernel::Config; -use kernel::global::{CURRENT_TX, LAST_COMPLETED_TASK, RECEIPTS, TASKS}; +use kernel::global::{CURRENT_TX, LAST_COMPLETED_TASK, RECEIPTS, STATE, TASKS}; +use kernel::memory::heap; use clibc::{log, logf}; use types::{KernelResult, TransactionReceipt}; @@ -61,9 +62,31 @@ pub(crate) fn write_kernel_result() { // physical address in the current setup. let ptr = encoded.as_ptr() as u32; core::mem::forget(encoded); + let (state_ptr, state_len) = match unsafe { STATE.get_mut().as_ref() } { + Some(state) => { + let len = state.encoded_len(); + if len == 0 { + (0, 0) + } else { + match heap::alloc(len, 8) { + Some(ptr) => { + let buf = unsafe { core::slice::from_raw_parts_mut(ptr, len) }; + match state.encode_into(buf) { + Some(written) => (ptr as u32, written as u32), + None => (0, 0), + } + } + None => (0, 0), + } + } + } + None => (0, 0), + }; let header = KernelResult { receipts_ptr: ptr, receipts_len: len, + state_ptr, + state_len, }; unsafe { core::ptr::write_volatile(Config::KERNEL_RESULT_ADDR as *mut KernelResult, header); diff --git a/crates/kernel/src/bundle/transfer.rs b/crates/kernel/src/bundle/transfer.rs new file mode 100644 index 0000000..510f5c2 --- /dev/null +++ b/crates/kernel/src/bundle/transfer.rs @@ -0,0 +1,27 @@ +use clibc::log; +use kernel::global::{CURRENT_TX, RECEIPTS, STATE}; +use state::State; +use types::Result; +use types::transaction::Transaction; + +const TRANSFER_ERROR: u32 = 1; + +pub(crate) fn transfer(tx: &Transaction) { + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let ok = state.transfer(&tx.from, &tx.to, tx.value); + if !ok { + log!("transfer failed"); + set_receipt(false, TRANSFER_ERROR); + } +} + +fn set_receipt(success: bool, error_code: u32) { + let tx_idx = unsafe { *CURRENT_TX.get_mut() }; + unsafe { + if let Some(receipts) = RECEIPTS.get_mut().as_mut() { + if let Some(receipt) = receipts.get_mut(tx_idx) { + receipt.result = Result::new(success, error_code); + } + } + } +} diff --git a/crates/kernel/src/syscall/balance.rs b/crates/kernel/src/syscall/balance.rs new file mode 100644 index 0000000..304f80e --- /dev/null +++ b/crates/kernel/src/syscall/balance.rs @@ -0,0 +1,92 @@ +use clibc::{log, logf}; +use types::{Address, ADDRESS_LEN}; + +use state::State; + +use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE}; +use crate::memory::page_allocator as mmu; +use crate::syscall::alloc::sys_alloc; +use crate::syscall::storage::{current_root_ppn, read_user_bytes}; +use crate::task::FROM_PTR_ADDR; + +pub(crate) fn sys_transfer(args: [u32; 6]) -> u32 { + let current = unsafe { *CURRENT_TASK.get_mut() }; + if current == KERNEL_TASK_SLOT { + log!("sys_transfer: kernel task not allowed"); + return 1; + } + + let root_ppn = match current_root_ppn() { + Some(root) => root, + None => return 1, + }; + + let to_ptr = args[1]; + let value = (args[2] as u64) | ((args[3] as u64) << 32); + + let from_bytes = match read_user_bytes(root_ppn, FROM_PTR_ADDR, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 1, + }; + let to_bytes = match read_user_bytes(root_ppn, to_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 1, + }; + if from_bytes.len() != ADDRESS_LEN || to_bytes.len() != ADDRESS_LEN { + log!("sys_transfer: invalid address length"); + return 1; + } + + let mut from_buf = [0u8; ADDRESS_LEN]; + let mut to_buf = [0u8; ADDRESS_LEN]; + from_buf.copy_from_slice(&from_bytes); + to_buf.copy_from_slice(&to_bytes); + let from = Address(from_buf); + let to = Address(to_buf); + + let state = unsafe { STATE.get_mut().get_or_insert_with(State::new) }; + let ok = state.transfer(&from, &to, value); + if ok { 0 } else { 1 } +} + +pub(crate) fn sys_balance(args: [u32; 6]) -> u32 { + let current = unsafe { *CURRENT_TASK.get_mut() }; + if current == KERNEL_TASK_SLOT { + log!("sys_balance: kernel task not allowed"); + return 0; + } + + let root_ppn = match current_root_ppn() { + Some(root) => root, + None => return 0, + }; + let addr_ptr = args[0]; + let address_bytes = match read_user_bytes(root_ppn, addr_ptr, ADDRESS_LEN) { + Some(bytes) => bytes, + None => return 0, + }; + if address_bytes.len() != ADDRESS_LEN { + log!("sys_balance: invalid address length"); + return 0; + } + let mut addr_buf = [0u8; ADDRESS_LEN]; + addr_buf.copy_from_slice(&address_bytes); + let address = Address(addr_buf); + + let balance = unsafe { STATE.get_mut() } + .as_ref() + .map(|state| state.balance_of(&address)) + .unwrap_or(0); + + let addr = sys_alloc([16, 8, 0, 0, 0, 0]); + if addr == 0 { + log!("sys_balance: allocation failed"); + return 0; + } + let bytes = balance.to_le_bytes(); + if !mmu::copy_into_user(root_ppn, addr, &bytes) { + logf!("sys_balance: failed to write to 0x%x", addr); + return 0; + } + addr +} diff --git a/crates/kernel/src/syscall/mod.rs b/crates/kernel/src/syscall/mod.rs index 7b244ce..4398c88 100644 --- a/crates/kernel/src/syscall/mod.rs +++ b/crates/kernel/src/syscall/mod.rs @@ -13,8 +13,10 @@ pub mod call_program; pub mod fire_event; pub mod panic; pub mod storage; +pub mod balance; use alloc::{sys_alloc, sys_dealloc}; +use balance::{sys_balance, sys_transfer}; use call_program::sys_call_program; use fire_event::sys_fire_event; use panic::sys_panic; @@ -55,16 +57,6 @@ pub fn dispatch_syscall(call_id: u32, args: [u32; 6], ctx: &mut SyscallContext<' } } -fn sys_transfer(_args: [u32; 6]) -> u32 { - log!("sys_transfer: need implementation"); - 0 -} - -fn sys_balance(_args: [u32; 6]) -> u32 { - log!("sys_balance: need implementation"); - 0 -} - fn sys_brk(_args: [u32; 6]) -> u32 { log!("sys_brk: need implementation"); 0 diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 2f2c608..27a67f8 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -100,7 +100,7 @@ const TRAMPOLINE_CODE: [u32; 2] = [ ]; pub(crate) const TO_PTR_ADDR: u32 = 0x120; -const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; +pub(crate) const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; pub(super) fn alloc_asid() -> u16 { diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index aab4ad1..db4a34d 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -6,6 +6,4 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -hex = { version = "0.4", default-features = false, features = ["alloc"] } types = { path = "../types" } # adjust path as needed -storage = { path = "../storage", optional = true } # adjust path as needed diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 8930e51..0a2707d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,8 +1,6 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] extern crate alloc; -#[cfg(feature = "std")] -extern crate std; pub mod types; pub mod account; diff --git a/crates/state/src/state.rs b/crates/state/src/state.rs index a48a430..c2adcb5 100644 --- a/crates/state/src/state.rs +++ b/crates/state/src/state.rs @@ -1,15 +1,8 @@ use alloc::collections::BTreeMap; -use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; -#[cfg(feature = "std")] -use std::rc::Rc; -#[cfg(feature = "std")] -use storage::Storage; use crate::Account; use types::address::Address; -#[cfg(feature = "std")] -use hex::encode as hex_encode; /// Represents the global state of the blockchain virtual machine. /// @@ -68,20 +61,6 @@ impl State { Self { accounts: BTreeMap::new() } } - /// Constructs a State from an existing Storage instance. - /// - /// EDUCATIONAL PURPOSE: This demonstrates how state can be reconstructed - /// from persistent storage. In real blockchains, the state is often - /// stored on disk and loaded into memory when needed. - /// - /// NOTE: This is currently a placeholder implementation that always - /// returns an empty state. In a real system, this would deserialize - /// the state from the provided storage. - #[cfg(feature = "std")] - pub fn new_from_storage(_storage: Rc) -> Self { - Self { accounts: BTreeMap::new() } - } - /// Retrieves an account by address (immutable reference). /// /// EDUCATIONAL PURPOSE: This demonstrates safe account access for reading. @@ -99,6 +78,14 @@ impl State { self.accounts.get(addr) } + /// Returns the current balance for an address (0 if missing). + pub fn balance_of(&self, addr: &Address) -> u128 { + self.accounts + .get(addr) + .map(|acc| acc.balance) + .unwrap_or(0) + } + /// Retrieves an account by address (mutable reference), creating it if it doesn't exist. /// /// EDUCATIONAL PURPOSE: This demonstrates account creation on-demand. @@ -141,6 +128,36 @@ impl State { }) } + /// Transfers native balance between accounts. Returns false on insufficient funds or overflow. + pub fn transfer(&mut self, from: &Address, to: &Address, value: u64) -> bool { + let amount = value as u128; + let from_balance = match self.get_account(from) { + Some(account) => account.balance, + None => return false, + }; + if from_balance < amount { + return false; + } + if from == to { + return true; + } + let to_balance = self.balance_of(to); + let new_to_balance = match to_balance.checked_add(amount) { + Some(balance) => balance, + None => return false, + }; + + { + let from_account = self.get_account_mut(from); + from_account.balance = from_balance - amount; + } + { + let to_account = self.get_account_mut(to); + to_account.balance = new_to_balance; + } + true + } + /// Checks if an address corresponds to a contract account. /// /// EDUCATIONAL PURPOSE: This demonstrates how to distinguish between @@ -162,27 +179,72 @@ impl State { /// Encode state into a byte buffer for guest consumption. pub fn encode(&self) -> alloc::vec::Vec { - let mut out = alloc::vec::Vec::new(); - out.extend_from_slice(&(self.accounts.len() as u32).to_le_bytes()); + let len = self.encoded_len(); + let mut out = alloc::vec![0u8; len]; + let _ = self.encode_into(&mut out); + out + } + + /// Returns the byte length of the encoded state. + pub fn encoded_len(&self) -> usize { + let mut total = 4usize; // account count + for (addr, acc) in &self.accounts { + let mut acc_len = 0usize; + acc_len = acc_len.saturating_add(addr.0.len()); + acc_len = acc_len.saturating_add(16); // balance + acc_len = acc_len.saturating_add(8); // nonce + acc_len = acc_len.saturating_add(1); // is_contract + acc_len = acc_len.saturating_add(4); // code len + acc_len = acc_len.saturating_add(acc.code.len()); + acc_len = acc_len.saturating_add(4); // storage len + for (k, v) in &acc.storage { + acc_len = acc_len.saturating_add(4); // key len + acc_len = acc_len.saturating_add(k.as_bytes().len()); + acc_len = acc_len.saturating_add(4); // val len + acc_len = acc_len.saturating_add(v.len()); + } + total = total.saturating_add(acc_len); + } + total + } + + /// Encode state into a provided buffer. Returns bytes written on success. + pub fn encode_into(&self, out: &mut [u8]) -> Option { + let mut cursor = 0usize; + let write = |buf: &mut [u8], cursor: &mut usize, bytes: &[u8]| -> Option<()> { + if *cursor + bytes.len() > buf.len() { + return None; + } + buf[*cursor..*cursor + bytes.len()].copy_from_slice(bytes); + *cursor += bytes.len(); + Some(()) + }; + + let count = self.accounts.len() as u32; + write(out, &mut cursor, &count.to_le_bytes())?; for (addr, acc) in &self.accounts { - out.extend_from_slice(&addr.0); - out.extend_from_slice(&acc.balance.to_le_bytes()); - out.extend_from_slice(&acc.nonce.to_le_bytes()); - out.push(acc.is_contract as u8); - out.extend_from_slice(&(acc.code.len() as u32).to_le_bytes()); - out.extend_from_slice(&acc.code); + write(out, &mut cursor, &addr.0)?; + write(out, &mut cursor, &acc.balance.to_le_bytes())?; + write(out, &mut cursor, &acc.nonce.to_le_bytes())?; + write(out, &mut cursor, &[acc.is_contract as u8])?; + let code_len = acc.code.len() as u32; + write(out, &mut cursor, &code_len.to_le_bytes())?; + write(out, &mut cursor, &acc.code)?; - out.extend_from_slice(&(acc.storage.len() as u32).to_le_bytes()); + let storage_len = acc.storage.len() as u32; + write(out, &mut cursor, &storage_len.to_le_bytes())?; for (k, v) in &acc.storage { - out.extend_from_slice(&(k.len() as u32).to_le_bytes()); - out.extend_from_slice(k.as_bytes()); - out.extend_from_slice(&(v.len() as u32).to_le_bytes()); - out.extend_from_slice(v); + let key_len = k.as_bytes().len() as u32; + write(out, &mut cursor, &key_len.to_le_bytes())?; + write(out, &mut cursor, k.as_bytes())?; + let val_len = v.len() as u32; + write(out, &mut cursor, &val_len.to_le_bytes())?; + write(out, &mut cursor, v)?; } } - out + Some(cursor) } /// Decode state produced by `encode`. @@ -272,137 +334,4 @@ impl State { Some(Self { accounts }) } - /// Deploys a contract to a specific address. - /// - /// EDUCATIONAL PURPOSE: This demonstrates smart contract deployment. - /// When a contract is deployed, it creates or updates an account with - /// the contract's bytecode and marks it as a contract account. - /// - /// DEPLOYMENT PROCESS: - /// 1. Get or create the account at the specified address - /// 2. Set the account's code to the provided bytecode - /// 3. Mark the account as a contract - /// - /// SECURITY: In real systems, contract deployment would include - /// additional checks like code validation, gas limits, etc. - /// - /// PARAMETERS: - /// - addr: The address where the contract should be deployed - /// - code: The bytecode of the contract to deploy - pub fn deploy_contract(&mut self, addr: Address, code: Vec) { - // EDUCATIONAL: Get or create the account at the specified address - let acc = self.accounts.entry(addr).or_insert_with(|| Account { - nonce: 0, // No transactions yet - balance: 0, // No initial balance - code: Vec::new(), // No code initially - is_contract: false, // Not a contract initially - storage: BTreeMap::new(), // Empty storage - }); - - // EDUCATIONAL: Set the contract code and mark as contract - acc.code = code; // Deploy the bytecode - acc.is_contract = true; // Mark as contract account - } - - /// Prints a human-readable representation of the current state. - /// - /// EDUCATIONAL PURPOSE: This demonstrates state inspection and debugging. - /// Being able to visualize the blockchain state is crucial for development, - /// testing, and understanding how transactions affect the system. - /// - /// OUTPUT FORMAT: Shows each account with its: - /// - Address (in hexadecimal) - /// - Balance - /// - Nonce (transaction count) - /// - Contract status - /// - Code size - /// - Storage contents - /// - /// USAGE: Useful for debugging, testing, and educational demonstrations. - #[cfg(feature = "std")] - pub fn pretty_print(&self) { - println!("--- State Dump ---"); - for (addr, acc) in &self.accounts { - // EDUCATIONAL: Display account address in hexadecimal format - println!(" 🔑 Address: 0x{}", hex_encode(addr.0)); - - // EDUCATIONAL: Display account metadata - println!(" - Balance: {}", acc.balance); - println!(" - Nonce: {}", acc.nonce); - println!(" - Is contract?: {}", acc.is_contract); - println!(" - Code size: {} bytes", acc.code.len()); - - // EDUCATIONAL: Display storage contents - println!(" - Storage:"); - for (key, value) in &acc.storage { - // EDUCATIONAL: Convert storage values to hexadecimal for readability - let value_hex: Vec = value.iter().map(|b| format!("{:02x}", b)).collect(); - - // Parse the key as "domain:key" format - if let Some((domain, key_part)) = Self::parse_domain_key(key) { - if domain == "P" { - // For persistent storage, treat key as ASCII - if let Ok(ascii_key) = String::from_utf8(hex::decode(&key_part).unwrap_or_default()) { - println!(" Key: {}:{} | Value ({} bytes): {}", domain, ascii_key, value.len(), value_hex.join(" ")); - } else { - println!(" Key: {}:{} | Value ({} bytes): {}", domain, key_part, value.len(), value_hex.join(" ")); - } - } else { - // For storage maps, treat domain as ASCII and key as hex - println!(" Key: {}:{} | Value ({} bytes): {}", domain, key_part, value.len(), value_hex.join(" ")); - } - } else { - // Fall back to showing the raw key - println!(" Key: {:<20} | Value ({} bytes): {}", key, value.len(), value_hex.join(" ")); - } - } - println!(); - } - println!("--------------------"); - } - - /// Parses a storage key in "domain:key" format to extract domain and key components. - /// - /// Storage keys are formatted as: "domain:key" - /// where domain is like "P" or "Balances" and key is hex-encoded - #[cfg(feature = "std")] - fn parse_domain_key(key: &str) -> Option<(String, String)> { - // Find the first colon to separate domain and key - if let Some(colon_pos) = key.find(':') { - let domain = key[..colon_pos].to_string(); - let key_part = key[colon_pos + 1..].to_string(); - return Some((domain, key_part)); - } - - None - } - - /// Parses a storage map key to extract address and domain components. - /// - /// Storage map keys are formatted as: [address_bytes][domain] - /// where address_bytes is 20 bytes and domain is like "-Balances" - fn parse_storage_map_key(key: &str) -> Option<(String, String)> { - // Check if the key is long enough to contain an address (20 bytes = 40 hex chars) - if key.len() < 40 { - return None; - } - - // Try to parse the first 40 characters as a hex address - if let Ok(address_bytes) = hex::decode(&key[..40]) { - if address_bytes.len() == 20 { - // Convert to proper address format - let address = format!("0x{}", &key[..40]); - - // Parse the domain (remaining hex characters) - let domain_hex = &key[40..]; - if let Ok(domain_bytes) = hex::decode(domain_hex) { - if let Ok(domain_str) = String::from_utf8(domain_bytes) { - return Some((address, domain_str)); - } - } - } - } - - None - } } diff --git a/crates/types/src/kernel_result.rs b/crates/types/src/kernel_result.rs index df1aacf..eff6edd 100644 --- a/crates/types/src/kernel_result.rs +++ b/crates/types/src/kernel_result.rs @@ -1,11 +1,13 @@ //! Kernel-to-bootloader handoff header for serialized receipts. -/// Pointer + length describing the receipts buffer in kernel memory. +/// Pointer + length describing kernel-owned output buffers. #[repr(C)] #[derive(Clone, Copy, Debug, Default)] pub struct KernelResult { pub receipts_ptr: u32, pub receipts_len: u32, + pub state_ptr: u32, + pub state_len: u32, } /// Kernel VA where the handoff header is written. From 823930752fb289a108e99ad632a35013c62f7335 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Thu, 1 Jan 2026 08:51:04 +0200 Subject: [PATCH 55/70] erc20 approvev and transfer from --- crates/compiler/tests/abi_generator_tests.rs | 20 +++++ crates/examples/src/dex.rs | 4 +- crates/examples/src/erc20.rs | 79 +++++++++++++++++++- crates/examples/tests/examples_test.rs | 2 +- 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/crates/compiler/tests/abi_generator_tests.rs b/crates/compiler/tests/abi_generator_tests.rs index 61d1227..7b7761d 100644 --- a/crates/compiler/tests/abi_generator_tests.rs +++ b/crates/compiler/tests/abi_generator_tests.rs @@ -76,6 +76,8 @@ fn test_function_extraction() { let function_names: Vec<&str> = abi.functions.iter().map(|f| f.name.as_str()).collect(); assert!(function_names.contains(&"init")); assert!(function_names.contains(&"transfer")); + assert!(function_names.contains(&"approve")); + assert!(function_names.contains(&"transfer_from")); assert!(function_names.contains(&"balance_of")); // Check selectors @@ -839,6 +841,24 @@ fn test_erc20_example_file_generates_typed_abi() { assert_eq!(transfer_func.inputs[1].name, "amount"); assert!(matches!(transfer_func.inputs[1].kind, ParamType::Uint(32))); + let approve_func = abi.functions.iter().find(|f| f.name == "approve").unwrap(); + assert_eq!(approve_func.selector, 3); + assert_eq!(approve_func.inputs.len(), 2); + assert_eq!(approve_func.inputs[0].name, "spender"); + assert!(matches!(approve_func.inputs[0].kind, ParamType::Address)); + assert_eq!(approve_func.inputs[1].name, "amount"); + assert!(matches!(approve_func.inputs[1].kind, ParamType::Uint(32))); + + let transfer_from = abi.functions.iter().find(|f| f.name == "transfer_from").unwrap(); + assert_eq!(transfer_from.selector, 4); + assert_eq!(transfer_from.inputs.len(), 3); + assert_eq!(transfer_from.inputs[0].name, "from"); + assert!(matches!(transfer_from.inputs[0].kind, ParamType::Address)); + assert_eq!(transfer_from.inputs[1].name, "to"); + assert!(matches!(transfer_from.inputs[1].kind, ParamType::Address)); + assert_eq!(transfer_from.inputs[2].name, "amount"); + assert!(matches!(transfer_from.inputs[2].kind, ParamType::Uint(32))); + let balance_func = abi.functions.iter().find(|f| f.name == "balance_of").unwrap(); assert_eq!(balance_func.selector, 5); assert_eq!(balance_func.inputs.len(), 1); diff --git a/crates/examples/src/dex.rs b/crates/examples/src/dex.rs index dcc06bd..614a393 100644 --- a/crates/examples/src/dex.rs +++ b/crates/examples/src/dex.rs @@ -86,7 +86,7 @@ fn add_liquidity(program: Address, caller: Address, mut parser: DataParser) -> R // Pull ERC20 from caller into the pool address. let ok = erc20 - .transfer(&caller, program, token_in as u32) + .transfer_from(&program, caller, program, token_in as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"add: token transfer failed"); @@ -225,7 +225,7 @@ fn swap(program: Address, caller: Address, mut parser: DataParser) -> Result { // Pull ERC20 into the pool. let ok = erc20 - .transfer(&caller, program, token_in as u32) + .transfer_from(&program, caller, program, token_in as u32) .map(|r| r.success) .unwrap_or(false); require(ok, b"swap: token transfer failed"); diff --git a/crates/examples/src/erc20.rs b/crates/examples/src/erc20.rs index 5bf72f7..c63ce68 100644 --- a/crates/examples/src/erc20.rs +++ b/crates/examples/src/erc20.rs @@ -3,10 +3,8 @@ extern crate clibc; use clibc::{ - DataParser, Map, entrypoint, event, fire_event, log, logf, persist_struct, require, - router::route, - types::{address::Address, o::O, result::Result}, - vm_panic, + DataParser, Map, StorageKey, entrypoint, event, fire_event, log, logf, persist_struct, + require, router::route, types::{address::Address, o::O, result::Result}, vm_panic, }; // Persistent structs @@ -27,6 +25,26 @@ event!(Transfer { }); Map!(Balances); +Map!(Allowances); + +struct AllowanceKey { + bytes: [u8; 40], +} + +impl AllowanceKey { + fn new(owner: Address, spender: Address) -> Self { + let mut bytes = [0u8; 40]; + bytes[..20].copy_from_slice(&owner.0); + bytes[20..].copy_from_slice(&spender.0); + Self { bytes } + } +} + +impl StorageKey for AllowanceKey { + fn as_storage_key(&self) -> &[u8] { + &self.bytes + } +} unsafe fn main_entry(program: Address, caller: Address, data: &[u8]) -> Result { route(data, program, caller, |to, from, call| { @@ -42,6 +60,21 @@ unsafe fn main_entry(program: Address, caller: Address, data: &[u8]) -> Result { transfer(&program, caller, to, amount); Result::new(true, 0) } + 0x03 => { + let mut parser = DataParser::new(call.args); + let spender = parser.read_address(); + let amount = parser.read_u32(); + approve(&program, caller, spender, amount); + Result::new(true, 0) + } + 0x04 => { + let mut parser = DataParser::new(call.args); + let from = parser.read_address(); + let to = parser.read_address(); + let amount = parser.read_u32(); + transfer_from(&program, caller, from, to, amount); + Result::new(true, 0) + } 0x05 => { let mut parser = DataParser::new(call.args); let owner = parser.read_address(); @@ -108,6 +141,44 @@ fn transfer(program: &Address, caller: Address, to: Address, amount: u32) { fire_event!(Transfer::new(caller, to, amount)); } +fn approve(program: &Address, caller: Address, spender: Address, amount: u32) { + let key = AllowanceKey::new(caller, spender); + Allowances::set(program, key, amount); +} + +fn transfer_from( + program: &Address, + caller: Address, + from: Address, + to: Address, + amount: u32, +) { + let allowance = match Allowances::get(program, AllowanceKey::new(from, caller)) { + O::Some(val) => val, + O::None => 0, + }; + require(allowance >= amount, b"allowance insufficient"); + + let from_bal = match Balances::get(program, from) { + O::Some(bal) => bal, + O::None => 0, + }; + if from_bal < amount { + vm_panic(b"insufficient"); + } + + let to_bal = match Balances::get(program, to) { + O::Some(bal) => bal, + O::None => 0, + }; + + Allowances::set(program, AllowanceKey::new(from, caller), allowance - amount); + Balances::set(program, from, from_bal - amount); + Balances::set(program, to, to_bal + amount); + + fire_event!(Transfer::new(from, to, amount)); +} + fn balance_of(program: &Address, owner: Address) -> u32 { match Balances::get(program, owner) { O::Some(bal) => bal, diff --git a/crates/examples/tests/examples_test.rs b/crates/examples/tests/examples_test.rs index 9cc4c2b..1c3f886 100644 --- a/crates/examples/tests/examples_test.rs +++ b/crates/examples/tests/examples_test.rs @@ -442,7 +442,7 @@ pub static TEST_CASES: Lazy>> = Lazy::new(|| { to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), data: encode_router_calls(&[HostFuncCall { - selector: 0x02, // transfer + selector: 0x03, // approve args: (|| { let mut args = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5") .0 From 69f32a35e5f1d6f21f14e8324d8c8b1b8ad3504a Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 2 Jan 2026 09:59:56 +0200 Subject: [PATCH 56/70] task: extract trampoline setup and rename mmu helpers Move trap/entry trampoline assembly and mapping into a dedicated module and replace the inline setup in prep. Document the trap trampoline instruction sequence for readability. Rename MMU helpers for shorter call sites (map_range_for_root, copy, translate) and update all kernel usages accordingly. --- crates/kernel/src/memory/page_allocator.rs | 16 ++-- crates/kernel/src/syscall/balance.rs | 2 +- crates/kernel/src/syscall/panic.rs | 2 +- crates/kernel/src/syscall/storage.rs | 4 +- crates/kernel/src/task/mod.rs | 1 + crates/kernel/src/task/prep.rs | 105 ++------------------- crates/kernel/src/task/trampoline.rs | 99 +++++++++++++++++++ crates/kernel/src/trap/mod.rs | 2 +- 8 files changed, 123 insertions(+), 108 deletions(-) create mode 100644 crates/kernel/src/task/trampoline.rs diff --git a/crates/kernel/src/memory/page_allocator.rs b/crates/kernel/src/memory/page_allocator.rs index 0232c0e..9ed9d5b 100644 --- a/crates/kernel/src/memory/page_allocator.rs +++ b/crates/kernel/src/memory/page_allocator.rs @@ -117,7 +117,7 @@ pub fn total_ppn() -> Option { } /// Map a user-visible virtual range with the provided permissions into a specific root. -pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { +pub fn map_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: PagePerms) -> bool { if len == 0 { return true; } @@ -141,7 +141,7 @@ pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: let available = alloc.remaining_ppn() as usize; if needed > available { panic!( - "map_user_range_for_root: out of physical memory (need {} pages, have {})", + "map_range_for_root: out of physical memory (need {} pages, have {})", needed, available ); } @@ -155,7 +155,7 @@ pub fn map_user_range_for_root(root_ppn: u32, va_start: u32, len: usize, perms: /// Map a user-visible virtual range with the provided permissions into the current root. pub fn map_user_range(va_start: u32, len: usize, perms: PagePerms) -> bool { let root = unsafe { *ROOT_PPN.get_mut() }; - map_user_range_for_root(root, va_start, len, perms) + map_range_for_root(root, va_start, len, perms) } /// Map a kernel-only virtual range with the provided permissions into a specific root. @@ -214,7 +214,7 @@ pub fn mirror_user_range_into_kernel(user_root: u32, va_start: u32, len: usize, }; let mut va = start; while va < end { - let phys = match translate_user_va(user_root, va) { + let phys = match translate(user_root, va) { Some(p) => p as u32, None => return false, }; @@ -227,7 +227,7 @@ pub fn mirror_user_range_into_kernel(user_root: u32, va_start: u32, len: usize, } /// Walk Sv32 to translate a VA in the given root to a physical address. -pub fn translate_user_va(root_ppn: u32, va: u32) -> Option { +pub fn translate(root_ppn: u32, va: u32) -> Option { let vpn1 = (va >> 22) & SV32_VPN_MASK; let vpn0 = (va >> 12) & SV32_VPN_MASK; let offset = (va & 0xfff) as usize; @@ -254,13 +254,13 @@ pub fn translate_user_va(root_ppn: u32, va: u32) -> Option { /// Peek a 32-bit value at a VA in a given root using the direct-map window. pub fn peek_word(root_ppn: u32, va: u32) -> Option { - let phys = translate_user_va(root_ppn, va)?; + let phys = translate(root_ppn, va)?; let va_ptr = direct_map_addr(phys)?; Some(unsafe { (va_ptr as *const u32).read_volatile() }) } /// Copy data into a user VA range for a specific root using the direct-map window. -pub fn copy_into_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { +pub fn copy(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { if data.is_empty() { return true; } @@ -268,7 +268,7 @@ pub fn copy_into_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { let mut src_off = 0usize; let mut va = va_start; while remaining > 0 { - let phys = match translate_user_va(root_ppn, va) { + let phys = match translate(root_ppn, va) { Some(p) => p, None => return false, }; diff --git a/crates/kernel/src/syscall/balance.rs b/crates/kernel/src/syscall/balance.rs index 304f80e..a50dab5 100644 --- a/crates/kernel/src/syscall/balance.rs +++ b/crates/kernel/src/syscall/balance.rs @@ -84,7 +84,7 @@ pub(crate) fn sys_balance(args: [u32; 6]) -> u32 { return 0; } let bytes = balance.to_le_bytes(); - if !mmu::copy_into_user(root_ppn, addr, &bytes) { + if !mmu::copy(root_ppn, addr, &bytes) { logf!("sys_balance: failed to write to 0x%x", addr); return 0; } diff --git a/crates/kernel/src/syscall/panic.rs b/crates/kernel/src/syscall/panic.rs index 63411c9..c1b1725 100644 --- a/crates/kernel/src/syscall/panic.rs +++ b/crates/kernel/src/syscall/panic.rs @@ -26,7 +26,7 @@ pub(crate) fn sys_panic_with_message(msg_ptr: u32, msg_len: u32) -> u32 { let mut dst_off = 0usize; let mut va = msg_ptr; while remaining > 0 { - let phys = match mmu::translate_user_va(root_ppn, va) { + let phys = match mmu::translate(root_ppn, va) { Some(p) => p, None => { logf!("sys_panic: invalid msg ptr 0x%x", va); diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index 4456acc..e1843c1 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -92,7 +92,7 @@ pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { buf.extend_from_slice(&(value.len() as u32).to_le_bytes()); buf.extend_from_slice(&value); - if !mmu::copy_into_user(root_ppn, addr, &buf) { + if !mmu::copy(root_ppn, addr, &buf) { logf!("sys_storage_get: failed to write to 0x%x", addr); return 0; } @@ -185,7 +185,7 @@ pub(crate) fn read_user_bytes(root_ppn: u32, ptr: u32, len: usize) -> Option 0 { - let phys = match mmu::translate_user_va(root_ppn, va) { + let phys = match mmu::translate(root_ppn, va) { Some(p) => p, None => { logf!("sys_storage: invalid memory access 0x%x", va); diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 27a67f8..24abc9f 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -55,6 +55,7 @@ use types::ADDRESS_LEN; pub mod task; pub mod prep; pub mod run; +mod trampoline; pub use task::{AddressSpace, Task, TrapFrame}; pub use prep::prep_program_task; diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 725f7a6..76d33d9 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,58 +1,14 @@ use crate::{AddressSpace, Config, Task}; -use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, TASKS}; +use crate::global::CURRENT_TASK; use crate::memory::page_allocator as mmu; use clibc::{log, logf}; use types::address::Address; use super::{ - alloc_asid, FROM_PTR_ADDR, HEAP_BYTES, INPUT_BASE_ADDR, PAGE_SIZE, PROGRAM_VA_BASE, - PROGRAM_WINDOW_BYTES, REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, - TO_PTR_ADDR, TRAMPOLINE_CODE, TRAMPOLINE_VA, TRAP_TRAMPOLINE_OFFSET, + alloc_asid, trampoline::map_trampoline_page, FROM_PTR_ADDR, INPUT_BASE_ADDR, PROGRAM_VA_BASE, + PROGRAM_WINDOW_BYTES, REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, TO_PTR_ADDR, }; -const REG_T0: u32 = 5; -const REG_T1: u32 = 6; -const REG_T2: u32 = 7; -const TRAP_TRAMPOLINE_WORDS: usize = 7; // csrr + 2x(hi/lo) + csrw + jalr - -fn split_imm(val: u32) -> (u32, i32) { - // Build a LUI/ADDI pair for a full 32-bit immediate. - let hi = ((val as u64 + 0x800) >> 12) as u32; - let lo = val as i64 - ((hi as i64) << 12); - (hi, lo as i32) -} - -fn encode_lui(rd: u32, imm20: u32) -> u32 { - (imm20 << 12) | (rd << 7) | 0x37 -} - -fn encode_addi(rd: u32, rs1: u32, imm12: i32) -> u32 { - ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x13 -} - -fn encode_jalr(rd: u32, rs1: u32, imm12: i32) -> u32 { - ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x67 -} - -fn encode_csrr(rd: u32, csr: u32) -> u32 { - (csr << 20) | (0 << 15) | (0b010 << 12) | (rd << 7) | 0x73 -} - -fn build_trap_trampoline(kernel_satp: u32, trap_entry: u32) -> [u32; TRAP_TRAMPOLINE_WORDS] { - // Trampoline swaps to the kernel root and jumps to trap_entry, preserving user satp in t0. - let (satp_hi, satp_lo) = split_imm(kernel_satp); - let (entry_hi, entry_lo) = split_imm(trap_entry); - [ - encode_csrr(REG_T0, 0x180), // csrr t0, satp - encode_lui(REG_T1, satp_hi), - encode_addi(REG_T1, REG_T1, satp_lo), - 0x1803_1073, // csrw satp, t1 - encode_lui(REG_T2, entry_hi), - encode_addi(REG_T2, REG_T2, entry_lo), - encode_jalr(0, REG_T2, 0), // jr t2 - ] -} - /// Create a new task for a program and map its virtual address window via syscalls. /// /// This sets up: @@ -91,7 +47,7 @@ pub fn prep_program_task( window_end ); let perms = mmu::PagePerms::user_rwx(); - if !mmu::map_user_range_for_root(root_ppn, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { + if !mmu::map_range_for_root(root_ppn, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { panic!("launch_program: mapping failed (root=0x{:x})", root_ppn); } @@ -117,20 +73,20 @@ pub fn prep_program_task( let nz_count = code.iter().filter(|&&b| b != 0).count(); let local_first_nz = code.iter().position(|&b| b != 0).unwrap_or(code.len()); - if !mmu::copy_into_user(root_ppn, PROGRAM_VA_BASE, code) { + if !mmu::copy(root_ppn, PROGRAM_VA_BASE, code) { logf!("launch_program: failed to copy code into root=0x%x", root_ppn); return None; } - if !mmu::copy_into_user(root_ppn, TO_PTR_ADDR, &to.0) { + if !mmu::copy(root_ppn, TO_PTR_ADDR, &to.0) { logf!("launch_program: failed to copy 'to' address into root=0x%x", root_ppn); return None; } - if !mmu::copy_into_user(root_ppn, FROM_PTR_ADDR, &from.0) { + if !mmu::copy(root_ppn, FROM_PTR_ADDR, &from.0) { logf!("launch_program: failed to copy 'from' address into root=0x%x", root_ppn); return None; } - if !mmu::copy_into_user(root_ppn, INPUT_BASE_ADDR, input) { + if !mmu::copy(root_ppn, INPUT_BASE_ADDR, input) { panic!( "prep_program_task: failed to copy input into root=0x{:x}", root_ppn @@ -139,7 +95,7 @@ pub fn prep_program_task( // Sanity check where the code landed in the user root. let entry_va = PROGRAM_VA_BASE.wrapping_add(entry_off); - let user_phys = mmu::translate_user_va(root_ppn, entry_va).unwrap_or(usize::MAX); + let user_phys = mmu::translate(root_ppn, entry_va).unwrap_or(usize::MAX); let user_word = mmu::peek_word(root_ppn, entry_va).unwrap_or(0); logf!( "prep_program_task: code VA=0x%x user_phys=0x%x user_word=0x%x code_start=0x%x", @@ -148,48 +104,7 @@ pub fn prep_program_task( user_word, entry_off ); - // Install a small trampoline page mapped in both roots so we can switch - // satp safely before jumping into the user program. - let tramp_perms = mmu::PagePerms::user_rwx(); - let kernel_root = unsafe { - TASKS - .get_mut() - .get(KERNEL_TASK_SLOT) - .map(|task| task.addr_space.root_ppn) - .unwrap_or_else(mmu::current_root) - }; - let trap_entry = crate::trap::trap_entry as usize as u32; - let trap_trampoline = build_trap_trampoline(kernel_root, trap_entry); - // Stash both trampolines in a single shared page. - let mut tramp_bytes = - [0u8; TRAP_TRAMPOLINE_OFFSET + TRAP_TRAMPOLINE_WORDS * 4]; - for (i, word) in TRAMPOLINE_CODE.iter().enumerate() { - tramp_bytes[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); - } - for (i, word) in trap_trampoline.iter().enumerate() { - // Trap stub lives at TRAP_TRAMPOLINE_OFFSET for stvec to target. - let base = TRAP_TRAMPOLINE_OFFSET + i * 4; - tramp_bytes[base..base + 4].copy_from_slice(&word.to_le_bytes()); - } - if !mmu::map_user_range_for_root(kernel_root, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { - panic!("prep_program_task: failed to map trampoline page in kernel root"); - } - if !mmu::copy_into_user(kernel_root, TRAMPOLINE_VA, &tramp_bytes) { - panic!("prep_program_task: failed to populate trampoline code"); - } - let tramp_phys = match mmu::translate_user_va(kernel_root, TRAMPOLINE_VA) { - Some(p) => p as u32, - None => panic!("prep_program_task: trampoline VA not mapped in kernel root"), - }; - if !mmu::map_physical_range_for_root( - root_ppn, - TRAMPOLINE_VA, - tramp_phys, - PAGE_SIZE, - tramp_perms, - ) { - panic!("prep_program_task: failed to map trampoline page in user root"); - } + map_trampoline_page(root_ppn); let mut task = Task::new( AddressSpace::new( diff --git a/crates/kernel/src/task/trampoline.rs b/crates/kernel/src/task/trampoline.rs new file mode 100644 index 0000000..acf6bf9 --- /dev/null +++ b/crates/kernel/src/task/trampoline.rs @@ -0,0 +1,99 @@ +use crate::global::{KERNEL_TASK_SLOT, TASKS}; +use crate::memory::page_allocator as mmu; + +use super::{ + PAGE_SIZE, TRAMPOLINE_CODE, TRAMPOLINE_VA, TRAP_TRAMPOLINE_OFFSET, +}; + +const REG_T0: u32 = 5; +const REG_T1: u32 = 6; +const REG_T2: u32 = 7; +const TRAP_TRAMPOLINE_WORDS: usize = 7; // csrr + 2x(hi/lo) + csrw + jalr + +fn split_imm(val: u32) -> (u32, i32) { + // Build a LUI/ADDI pair for a full 32-bit immediate. + let hi = ((val as u64 + 0x800) >> 12) as u32; + let lo = val as i64 - ((hi as i64) << 12); + (hi, lo as i32) +} + +fn encode_lui(rd: u32, imm20: u32) -> u32 { + (imm20 << 12) | (rd << 7) | 0x37 +} + +fn encode_addi(rd: u32, rs1: u32, imm12: i32) -> u32 { + ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x13 +} + +fn encode_jalr(rd: u32, rs1: u32, imm12: i32) -> u32 { + ((imm12 as u32 & 0xfff) << 20) | (rs1 << 15) | (rd << 7) | 0x67 +} + +fn encode_csrr(rd: u32, csr: u32) -> u32 { + (csr << 20) | (0 << 15) | (0b010 << 12) | (rd << 7) | 0x73 +} + +/// Build the trap-entry trampoline instructions. +/// +/// This stub runs at `TRAP_TRAMPOLINE_VA` while still in the user address space. +/// It saves the current user `satp` into `t0`, switches to the kernel root page +/// table, and jumps to the real kernel trap handler at `trap_entry`. +fn build_trap_trampoline(kernel_satp: u32, trap_entry: u32) -> [u32; TRAP_TRAMPOLINE_WORDS] { + // Assemble the kernel satp and trap_entry as LUI/ADDI pairs. + let (satp_hi, satp_lo) = split_imm(kernel_satp); + let (entry_hi, entry_lo) = split_imm(trap_entry); + [ + encode_csrr(REG_T0, 0x180), // csrr t0, satp: save user satp so kernel can restore later. + encode_lui(REG_T1, satp_hi), // lui t1, %hi(kernel_satp): load upper bits. + encode_addi(REG_T1, REG_T1, satp_lo), // addi t1, t1, %lo(kernel_satp): finish satp. + 0x1803_1073, // csrw satp, t1: switch to kernel page table. + encode_lui(REG_T2, entry_hi), // lui t2, %hi(trap_entry): load trap handler addr. + encode_addi(REG_T2, REG_T2, entry_lo), // addi t2, t2, %lo(trap_entry). + encode_jalr(0, REG_T2, 0), // jalr x0, t2, 0: jump to trap handler. + ] +} + +pub(super) fn map_trampoline_page(root_ppn: u32) { + // Install a small trampoline page mapped in both roots so we can switch + // satp safely before jumping into the user program. + let tramp_perms = mmu::PagePerms::user_rwx(); + let kernel_root = unsafe { + TASKS + .get_mut() + .get(KERNEL_TASK_SLOT) + .map(|task| task.addr_space.root_ppn) + .unwrap_or_else(mmu::current_root) + }; + let trap_entry = crate::trap::trap_entry as usize as u32; + let trap_trampoline = build_trap_trampoline(kernel_root, trap_entry); + // Stash both trampolines in a single shared page. + let mut tramp_bytes = + [0u8; TRAP_TRAMPOLINE_OFFSET + TRAP_TRAMPOLINE_WORDS * 4]; + for (i, word) in TRAMPOLINE_CODE.iter().enumerate() { + tramp_bytes[i * 4..(i + 1) * 4].copy_from_slice(&word.to_le_bytes()); + } + for (i, word) in trap_trampoline.iter().enumerate() { + // Trap stub lives at TRAP_TRAMPOLINE_OFFSET for stvec to target. + let base = TRAP_TRAMPOLINE_OFFSET + i * 4; + tramp_bytes[base..base + 4].copy_from_slice(&word.to_le_bytes()); + } + if !mmu::map_range_for_root(kernel_root, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { + panic!("prep_program_task: failed to map trampoline page in kernel root"); + } + if !mmu::copy(kernel_root, TRAMPOLINE_VA, &tramp_bytes) { + panic!("prep_program_task: failed to populate trampoline code"); + } + let tramp_phys = match mmu::translate(kernel_root, TRAMPOLINE_VA) { + Some(p) => p as u32, + None => panic!("prep_program_task: trampoline VA not mapped in kernel root"), + }; + if !mmu::map_physical_range_for_root( + root_ppn, + TRAMPOLINE_VA, + tramp_phys, + PAGE_SIZE, + tramp_perms, + ) { + panic!("prep_program_task: failed to map trampoline page in user root"); + } +} diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index 1c513f4..f36487d 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -295,7 +295,7 @@ fn write_result_to_caller(caller_task: &mut Task, result: &VmResult) -> Option 0 { buf[9..9 + data_len].copy_from_slice(&result.data[..data_len]); } - if !mmu::copy_into_user(caller_task.addr_space.root_ppn, addr, &buf) { + if !mmu::copy(caller_task.addr_space.root_ppn, addr, &buf) { return None; } Some(addr) From 10f8dd11ffc7cc43c6d105cc3c15b40c6b1d42df Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 2 Jan 2026 21:17:03 +0200 Subject: [PATCH 57/70] WIP memory fixes --- crates/kernel/src/task/mod.rs | 2 +- crates/kernel/src/task/trampoline.rs | 16 ++++++++++++---- crates/kernel/src/trap/mod.rs | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 24abc9f..1c6fdf4 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -70,7 +70,7 @@ pub const PROGRAM_VA_BASE: u32 = 0x0; // is mapped into both roots so satp can be switched without invalidating the // instruction stream mid-flight. pub const TRAMPOLINE_VA: u32 = - (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES) as u32; // Shared page just past user window. + (PROGRAM_VA_BASE as usize + PROGRAM_WINDOW_BYTES + 0x10000) as u32; // Shared page outside the kernel window. const TRAP_TRAMPOLINE_OFFSET: usize = 0x10; // Offset for the trap-entry stub within the page. pub const TRAP_TRAMPOLINE_VA: u32 = TRAMPOLINE_VA + TRAP_TRAMPOLINE_OFFSET as u32; // stvec target for user-mode traps. diff --git a/crates/kernel/src/task/trampoline.rs b/crates/kernel/src/task/trampoline.rs index acf6bf9..1a91b24 100644 --- a/crates/kernel/src/task/trampoline.rs +++ b/crates/kernel/src/task/trampoline.rs @@ -56,7 +56,8 @@ fn build_trap_trampoline(kernel_satp: u32, trap_entry: u32) -> [u32; TRAP_TRAMPO pub(super) fn map_trampoline_page(root_ppn: u32) { // Install a small trampoline page mapped in both roots so we can switch // satp safely before jumping into the user program. - let tramp_perms = mmu::PagePerms::user_rwx(); + let kernel_tramp_perms = mmu::PagePerms::kernel_rwx(); + let user_tramp_perms = mmu::PagePerms::new(true, false, true, true); let kernel_root = unsafe { TASKS .get_mut() @@ -77,7 +78,12 @@ pub(super) fn map_trampoline_page(root_ppn: u32) { let base = TRAP_TRAMPOLINE_OFFSET + i * 4; tramp_bytes[base..base + 4].copy_from_slice(&word.to_le_bytes()); } - if !mmu::map_range_for_root(kernel_root, TRAMPOLINE_VA, PAGE_SIZE, tramp_perms) { + if !mmu::map_range_for_root( + kernel_root, + TRAMPOLINE_VA, + PAGE_SIZE, + kernel_tramp_perms, + ) { panic!("prep_program_task: failed to map trampoline page in kernel root"); } if !mmu::copy(kernel_root, TRAMPOLINE_VA, &tramp_bytes) { @@ -85,14 +91,16 @@ pub(super) fn map_trampoline_page(root_ppn: u32) { } let tramp_phys = match mmu::translate(kernel_root, TRAMPOLINE_VA) { Some(p) => p as u32, - None => panic!("prep_program_task: trampoline VA not mapped in kernel root"), + None => { + panic!("prep_program_task: trampoline VA not mapped in kernel root"); + } }; if !mmu::map_physical_range_for_root( root_ppn, TRAMPOLINE_VA, tramp_phys, PAGE_SIZE, - tramp_perms, + user_tramp_perms, ) { panic!("prep_program_task: failed to map trampoline page in user root"); } diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index f36487d..7b4eb88 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -58,12 +58,15 @@ pub unsafe extern "C" fn trap_entry() -> ! { "call {swap} # switch to kernel stack and reserve trap frame", "call {save} # save regs on kernel stack", "mv s0, a0 # preserve trap frame pointer across handle_trap", + "call {ensure} # restore kernel root if we trapped from user", + "mv a0, s0 # restore trap frame pointer for handle_trap", "call {handler} # run Rust trap handler", "mv a2, a1 # stash return kind", "mv a1, s0 # restore trap frame pointer for restore", "j {restore}", swap = sym swap_to_kernel_stack, save = sym save_trap_frame, + ensure = sym ensure_kernel_root_for_trap, handler = sym handle_trap, restore = sym restore_trap_frame, options(noreturn), @@ -228,6 +231,22 @@ pub extern "C" fn handle_trap(saved: *mut u32) -> (u32, u32) { (return_sp, return_kind) } +#[unsafe(no_mangle)] +/// Restore the kernel address-space root for traps arriving from user mode. +extern "C" fn ensure_kernel_root_for_trap() { + if read_sstatus() & SSTATUS_SPP != 0 { + return; + } + let kernel_root = unsafe { + TASKS + .get_mut() + .get(KERNEL_TASK_SLOT) + .map(|task| task.addr_space.root_ppn) + .unwrap_or_else(mmu::current_root) + }; + mmu::set_current_root(kernel_root); +} + #[inline(always)] fn read_scause() -> usize { let value: usize; From 60b7e5892ff8b86cabcdbb24ad9b29e02037b782 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 2 Jan 2026 21:31:54 +0200 Subject: [PATCH 58/70] kernel: centralize config constants in global Move kernel config and ABI address constants into global.rs and update all call sites to use the new globals. Remove the Config module/export and delete config.rs now that constants live in global. Reorganize global.rs with prominent section headers and add/adjust comments for clarity. Also update clibc entrypoint comment to point at the new RESULT_ADDR location. --- crates/clibc/src/entrypoint.rs | 2 +- crates/kernel/src/bundle/create_account.rs | 5 +-- crates/kernel/src/bundle/result.rs | 7 ++-- crates/kernel/src/config.rs | 14 ------- crates/kernel/src/global.rs | 43 ++++++++++++++++++++++ crates/kernel/src/lib.rs | 2 - crates/kernel/src/syscall/balance.rs | 2 +- crates/kernel/src/syscall/call_program.rs | 5 +-- crates/kernel/src/syscall/storage.rs | 2 +- crates/kernel/src/task/mod.rs | 10 +---- crates/kernel/src/task/prep.rs | 19 ++++++---- crates/kernel/src/trap/mod.rs | 12 +++--- crates/kernel/src/user_program.rs | 5 +-- 13 files changed, 76 insertions(+), 52 deletions(-) delete mode 100644 crates/kernel/src/config.rs diff --git a/crates/clibc/src/entrypoint.rs b/crates/clibc/src/entrypoint.rs index b96658b..ff274ed 100644 --- a/crates/clibc/src/entrypoint.rs +++ b/crates/clibc/src/entrypoint.rs @@ -43,7 +43,7 @@ macro_rules! entrypoint { ) { // EDUCATIONAL: Write result directly to predetermined memory location // This prevents conflicts with macros that might overwrite A4 - // Must match Config::RESULT_ADDR in crates/avm/src/global.rs + // Must match global::RESULT_ADDR in crates/kernel/src/global.rs const RESULT_ADDR: usize = 0x100; // Predetermined memory address for result object // EDUCATIONAL: Convert raw pointer to contract address diff --git a/crates/kernel/src/bundle/create_account.rs b/crates/kernel/src/bundle/create_account.rs index 420b5f4..f7ba086 100644 --- a/crates/kernel/src/bundle/create_account.rs +++ b/crates/kernel/src/bundle/create_account.rs @@ -1,5 +1,4 @@ -use kernel::global::STATE; -use kernel::Config; +use kernel::global::{CODE_SIZE_LIMIT, RO_DATA_SIZE_LIMIT, STATE}; use clibc::logf; use clibc::parser::HexCodec; use state::State; @@ -19,7 +18,7 @@ pub(crate) fn create_account(tx: &Transaction) { code_size as u32 ); - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + let max = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT; if code_size > max { panic!( "❌ Code size ({}) exceeds CODE_SIZE_LIMIT ({} bytes)", diff --git a/crates/kernel/src/bundle/result.rs b/crates/kernel/src/bundle/result.rs index 711e832..bf2c5be 100644 --- a/crates/kernel/src/bundle/result.rs +++ b/crates/kernel/src/bundle/result.rs @@ -1,5 +1,6 @@ -use kernel::Config; -use kernel::global::{CURRENT_TX, LAST_COMPLETED_TASK, RECEIPTS, STATE, TASKS}; +use kernel::global::{ + CURRENT_TX, KERNEL_RESULT_ADDR, LAST_COMPLETED_TASK, RECEIPTS, STATE, TASKS, +}; use kernel::memory::heap; use clibc::{log, logf}; use types::{KernelResult, TransactionReceipt}; @@ -89,7 +90,7 @@ pub(crate) fn write_kernel_result() { state_len, }; unsafe { - core::ptr::write_volatile(Config::KERNEL_RESULT_ADDR as *mut KernelResult, header); + core::ptr::write_volatile(KERNEL_RESULT_ADDR as *mut KernelResult, header); } logf!( "kernel_result: receipts_ptr=0x%x receipts_len=%d", diff --git a/crates/kernel/src/config.rs b/crates/kernel/src/config.rs deleted file mode 100644 index b71a1d8..0000000 --- a/crates/kernel/src/config.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub struct Config; - -impl Config { - pub const MAX_INPUT_LEN: usize = 1024; - pub const CODE_SIZE_LIMIT: usize = 0x30000; // 192KB headroom for non-compressed RV32IM binaries - pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; // 8KB for read-only data - pub const HEAP_START_ADDR: usize = Self::CODE_SIZE_LIMIT + Self::RO_DATA_SIZE_LIMIT + 0x100; - pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; - - pub const PROGRAM_START_ADDR: u32 = 0x400; - pub const RESULT_ADDR: u32 = 0x100; - /// Kernel handoff header address for serialized receipts (kernel VA). - pub const KERNEL_RESULT_ADDR: u32 = 0x100; -} diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 683b0aa..6eb21ee 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -6,6 +6,7 @@ use core::mem::MaybeUninit; use core::ptr; use state::State; use types::TransactionReceipt; +use types::ADDRESS_LEN; use types::transaction::TransactionBundle; use crate::Task; @@ -35,6 +36,35 @@ impl Global { unsafe impl Sync for Global {} +// ============================================ +// Program Call Limits and Memory Layout +// ============================================ +/// Maximum input buffer length accepted by program calls. +pub const MAX_INPUT_LEN: usize = 1024; +/// Upper bound for program text + data bytes in a user image. +pub const CODE_SIZE_LIMIT: usize = 0x30000; +/// Reserved space for read-only data in the user window. +pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; +/// Start of the user heap within the program window. +pub const HEAP_START_ADDR: usize = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + 0x100; +/// Maximum size of a program result payload. +pub const MAX_RESULT_SIZE: usize = types::result::RESULT_SIZE; +/// Default program entry address within the user window. +pub const PROGRAM_START_ADDR: u32 = 0x400; +/// Address where program results are written for user-mode reads. +pub const RESULT_ADDR: u32 = 0x100; +/// Kernel VA for the serialized result header handoff. +pub const KERNEL_RESULT_ADDR: u32 = 0x100; +/// User VA where the "to" address bytes are copied for program calls. +pub(crate) const TO_PTR_ADDR: u32 = 0x120; +/// User VA where the "from" address bytes are copied for program calls. +pub(crate) const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; +/// User VA base for the input buffer in the program heap window. +pub(crate) const INPUT_BASE_ADDR: u32 = HEAP_START_ADDR as u32; + +// ============================================ +// Task Scheduling and Bookkeeping +// ============================================ /// Max number of task slots the kernel tracks at once. pub const MAX_TASKS: usize = 16; /// Reserved slot index for the kernel/supervisor task. @@ -51,6 +81,10 @@ pub static RECEIPTS: Global>> = Global::new(None) /// Currently decoded bundle, if any. pub static BUNDLE: Global> = Global::new(None); +// ============================================ +// Task List Storage +// ============================================ +/// Fixed-size task list backing store for scheduler bookkeeping. pub struct TaskList { len: usize, slots: MaybeUninit<[Task; MAX_TASKS]>, @@ -139,10 +173,19 @@ impl Drop for TaskList { } } +// ============================================ +// Global Kernel State +// ============================================ #[allow(dead_code)] +/// Global task list storage. pub static TASKS: Global = Global::new(TaskList::new()); +/// Global chain state snapshot, if loaded. pub static STATE: Global> = Global::new(None); +/// Next ASID to assign when launching a program. pub static NEXT_ASID: Global = Global::new(1); +/// Root physical page number for the kernel address space. pub static ROOT_PPN: Global = Global::new(0); +/// Page allocator backing store. pub static PAGE_ALLOC: Global> = Global::new(None); +/// Kernel heap allocator instance. pub static KERNEL_HEAP: Global = Global::new(BumpAllocator::empty()); diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index c5acc41..c084723 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -2,8 +2,6 @@ #![feature(naked_functions)] #![feature(alloc_error_handler)] -pub mod config; -pub use config::Config; pub use types::boot::BootInfo; pub mod global; pub mod task; diff --git a/crates/kernel/src/syscall/balance.rs b/crates/kernel/src/syscall/balance.rs index a50dab5..bd1caf3 100644 --- a/crates/kernel/src/syscall/balance.rs +++ b/crates/kernel/src/syscall/balance.rs @@ -7,7 +7,7 @@ use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE}; use crate::memory::page_allocator as mmu; use crate::syscall::alloc::sys_alloc; use crate::syscall::storage::{current_root_ppn, read_user_bytes}; -use crate::task::FROM_PTR_ADDR; +use crate::global::FROM_PTR_ADDR; pub(crate) fn sys_transfer(args: [u32; 6]) -> u32 { let current = unsafe { *CURRENT_TASK.get_mut() }; diff --git a/crates/kernel/src/syscall/call_program.rs b/crates/kernel/src/syscall/call_program.rs index f2e4539..9633cdd 100644 --- a/crates/kernel/src/syscall/call_program.rs +++ b/crates/kernel/src/syscall/call_program.rs @@ -1,12 +1,11 @@ use clibc::logf; use types::{Address, ADDRESS_LEN}; -use crate::global::{CURRENT_TASK, TASKS}; +use crate::global::{CURRENT_TASK, MAX_INPUT_LEN, TASKS}; use crate::syscall::storage::{caller_address_matches, current_root_ppn, read_user_bytes}; use crate::syscall::SyscallContext; use crate::task::prep_program_task; use crate::user_program::with_program_image; -use crate::Config; const REG_COUNT: usize = 32; const REG_PC: usize = 32; @@ -17,7 +16,7 @@ pub(crate) fn sys_call_program(args: [u32; 6], ctx: &mut SyscallContext<'_>) -> let input_ptr = args[2]; let input_len = args[3] as usize; - if input_len > Config::MAX_INPUT_LEN { + if input_len > MAX_INPUT_LEN { logf!("sys_call_program: input too large"); return 0; } diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index e1843c1..cdb2f44 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -9,7 +9,7 @@ use types::{Address, ADDRESS_LEN, SV32_DIRECT_MAP_BASE, SV32_PAGE_SIZE}; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE, TASKS}; use crate::memory::page_allocator as mmu; use crate::syscall::alloc::sys_alloc; -use crate::task::TO_PTR_ADDR; +use crate::global::TO_PTR_ADDR; use state::State; pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 1c6fdf4..91d93ca 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -48,9 +48,7 @@ // - We currently do not touch sstatus/mstatus or perform sfence.vma; add those // when modeling fuller privilege transitions. -use crate::Config; -use crate::global::NEXT_ASID; -use types::ADDRESS_LEN; +use crate::global::{CODE_SIZE_LIMIT, NEXT_ASID, RO_DATA_SIZE_LIMIT}; pub mod task; pub mod prep; @@ -80,7 +78,7 @@ const fn align_up(val: usize, align: usize) -> usize { /// Total mapped window for a program: code/rodata, stack, and heap. pub const PROGRAM_WINDOW_BYTES: usize = align_up( - Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, + CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, PAGE_SIZE, ); @@ -100,10 +98,6 @@ const TRAMPOLINE_CODE: [u32; 2] = [ 0x1020_0073, // sret ]; -pub(crate) const TO_PTR_ADDR: u32 = 0x120; -pub(crate) const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; -const INPUT_BASE_ADDR: u32 = Config::HEAP_START_ADDR as u32; - pub(super) fn alloc_asid() -> u16 { unsafe { let counter = NEXT_ASID.get_mut(); diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 76d33d9..b216d14 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,12 +1,15 @@ -use crate::{AddressSpace, Config, Task}; -use crate::global::CURRENT_TASK; +use crate::{AddressSpace, Task}; +use crate::global::{ + CODE_SIZE_LIMIT, CURRENT_TASK, FROM_PTR_ADDR, HEAP_START_ADDR, INPUT_BASE_ADDR, MAX_INPUT_LEN, + RO_DATA_SIZE_LIMIT, TO_PTR_ADDR, +}; use crate::memory::page_allocator as mmu; use clibc::{log, logf}; use types::address::Address; use super::{ - alloc_asid, trampoline::map_trampoline_page, FROM_PTR_ADDR, INPUT_BASE_ADDR, PROGRAM_VA_BASE, - PROGRAM_WINDOW_BYTES, REG_A0, REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, TO_PTR_ADDR, + alloc_asid, trampoline::map_trampoline_page, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, REG_A0, + REG_A1, REG_A2, REG_A3, REG_SP, STACK_BYTES, }; /// Create a new task for a program and map its virtual address window via syscalls. @@ -24,7 +27,7 @@ pub fn prep_program_task( input: &[u8], entry_off: u32, ) -> Option { - if input.len() > Config::MAX_INPUT_LEN { + if input.len() > MAX_INPUT_LEN { log!("launch_program: input too large"); return None; } @@ -113,13 +116,13 @@ pub fn prep_program_task( PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES as u32, ), - Config::HEAP_START_ADDR as u32, + HEAP_START_ADDR as u32, ); let caller = unsafe { *CURRENT_TASK.get_mut() }; task.caller_task_id = Some(caller); // Set up initial trapframe. let stack_top = PROGRAM_VA_BASE - .wrapping_add((Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); + .wrapping_add((CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); task.tf.pc = entry_va; task.tf.regs[REG_SP] = stack_top; task.tf.regs[REG_A0] = TO_PTR_ADDR; @@ -141,7 +144,7 @@ pub fn prep_program_task( "prep_program_task: stack window=[0x%x,0x%x) heap_base=0x%x", stack_base, stack_top, - Config::HEAP_START_ADDR as u32 + HEAP_START_ADDR as u32 ); Some(task) diff --git a/crates/kernel/src/trap/mod.rs b/crates/kernel/src/trap/mod.rs index 7b4eb88..ffa5b0c 100644 --- a/crates/kernel/src/trap/mod.rs +++ b/crates/kernel/src/trap/mod.rs @@ -2,13 +2,15 @@ use core::arch::asm; use clibc::{log, logf}; use types::result::{Result as VmResult, RESULT_DATA_SIZE}; -use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, LAST_COMPLETED_TASK, TASKS}; +use crate::global::{ + CURRENT_TASK, KERNEL_TASK_SLOT, LAST_COMPLETED_TASK, MAX_RESULT_SIZE, RESULT_ADDR, TASKS, +}; use crate::memory::page_allocator as mmu; use crate::syscall; use crate::syscall::alloc::alloc_in_task; use crate::syscall::storage::read_user_bytes; use crate::task::TRAMPOLINE_VA; -use crate::{Config, Task}; +use crate::Task; mod save_trap_frame; mod restore_trap_frame; @@ -270,7 +272,7 @@ fn read_sstatus() -> u32 { fn read_task_result(task: &Task) -> Option { let result_bytes = - read_user_bytes(task.addr_space.root_ppn, Config::RESULT_ADDR, Config::MAX_RESULT_SIZE)?; + read_user_bytes(task.addr_space.root_ppn, RESULT_ADDR, MAX_RESULT_SIZE)?; if result_bytes.len() < 9 { return None; } @@ -305,8 +307,8 @@ fn log_task_result(result: &VmResult) { } fn write_result_to_caller(caller_task: &mut Task, result: &VmResult) -> Option { - let addr = alloc_in_task(caller_task, Config::MAX_RESULT_SIZE as u32, 4)?; - let mut buf = [0u8; Config::MAX_RESULT_SIZE]; + let addr = alloc_in_task(caller_task, MAX_RESULT_SIZE as u32, 4)?; + let mut buf = [0u8; MAX_RESULT_SIZE]; buf[0] = result.success as u8; buf[1..5].copy_from_slice(&result.error_code.to_le_bytes()); buf[5..9].copy_from_slice(&result.data_len.to_le_bytes()); diff --git a/crates/kernel/src/user_program.rs b/crates/kernel/src/user_program.rs index aea05d5..c3ec9da 100644 --- a/crates/kernel/src/user_program.rs +++ b/crates/kernel/src/user_program.rs @@ -5,8 +5,7 @@ use clibc::logf; use state::State; use types::address::Address; -use crate::global::STATE; -use crate::Config; +use crate::global::{CODE_SIZE_LIMIT, RO_DATA_SIZE_LIMIT, STATE}; pub struct ProgramImage<'a> { pub code: &'a [u8], @@ -64,7 +63,7 @@ pub fn with_program_image( // Enforce the code size limit to prevent oversized binaries. let code_len = account.code.len(); - let max = Config::CODE_SIZE_LIMIT + Config::RO_DATA_SIZE_LIMIT; + let max = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT; if code_len > max { panic!( "❌ Program call rejected: code size ({}) exceeds limit ({})", From a28adfdfdcd2b8db541bb1c58578b55d8f8033f0 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 2 Jan 2026 23:05:14 +0200 Subject: [PATCH 59/70] Fix user window layout and expand kernel heap - place stack at end of program window to avoid heap overlap - add dedicated call-args page above user window and remap TO/FROM/INPUT - centralize program window sizing in global and reuse in task module - rename current_root_ppn to current_task_root_ppn for clarity - expand kernel VA window and test VM memory defaults --- crates/bootloader/src/bootloader.rs | 2 +- crates/examples/tests/common/test_runner.rs | 2 +- crates/kernel/src/global.rs | 25 +++++++++++++++++---- crates/kernel/src/syscall/balance.rs | 6 ++--- crates/kernel/src/syscall/call_program.rs | 4 ++-- crates/kernel/src/syscall/fire_event.rs | 4 ++-- crates/kernel/src/syscall/storage.rs | 6 ++--- crates/kernel/src/task/mod.rs | 23 ++++++------------- crates/kernel/src/task/prep.rs | 15 +++++++++---- 9 files changed, 51 insertions(+), 36 deletions(-) diff --git a/crates/bootloader/src/bootloader.rs b/crates/bootloader/src/bootloader.rs index 7a9a626..2c8937c 100644 --- a/crates/bootloader/src/bootloader.rs +++ b/crates/bootloader/src/bootloader.rs @@ -13,7 +13,7 @@ use vm::memory::{API, Perms, Sv32Memory, HEAP_PTR_OFFSET, Memory as MmuRef, Virt use vm::registers::Register; use vm::vm::VM; -const KERNEL_WINDOW_BYTES: usize = 256 * 1024; +const KERNEL_WINDOW_BYTES: usize = 4 * 1024 * 1024; const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; /// Boot configuration options consumed by the loader. diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs index 9870fbc..cbda489 100644 --- a/crates/examples/tests/common/test_runner.rs +++ b/crates/examples/tests/common/test_runner.rs @@ -83,7 +83,7 @@ impl TestRunner { TestRunner { writer, verbose: false, - vm_memory_size: 4 * 1024 * 1024, // larger default to accommodate bigger binaries without RVC + vm_memory_size: 16 * 1024 * 1024, // larger default to accommodate bigger binaries without RVC kernel_bytes: Self::load_kernel_from_env(), kernel_path: env::var("KERNEL_ELF").ok(), } diff --git a/crates/kernel/src/global.rs b/crates/kernel/src/global.rs index 6eb21ee..379d796 100644 --- a/crates/kernel/src/global.rs +++ b/crates/kernel/src/global.rs @@ -6,7 +6,7 @@ use core::mem::MaybeUninit; use core::ptr; use state::State; use types::TransactionReceipt; -use types::ADDRESS_LEN; +use types::{ADDRESS_LEN, SV32_PAGE_SIZE}; use types::transaction::TransactionBundle; use crate::Task; @@ -45,6 +45,17 @@ pub const MAX_INPUT_LEN: usize = 1024; pub const CODE_SIZE_LIMIT: usize = 0x30000; /// Reserved space for read-only data in the user window. pub const RO_DATA_SIZE_LIMIT: usize = 0x2000; +/// User VA base for program mappings. +pub const PROGRAM_VA_BASE: u32 = 0x0; +/// User stack size (bytes). +pub const STACK_BYTES: usize = 0x4000; // 16 KiB user stack +/// User heap size (bytes). +pub const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap +/// Total mapped window for a program: code/rodata, stack, and heap. +pub const PROGRAM_WINDOW_BYTES: usize = align_up( + CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, + SV32_PAGE_SIZE, +); /// Start of the user heap within the program window. pub const HEAP_START_ADDR: usize = CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + 0x100; /// Maximum size of a program result payload. @@ -55,12 +66,14 @@ pub const PROGRAM_START_ADDR: u32 = 0x400; pub const RESULT_ADDR: u32 = 0x100; /// Kernel VA for the serialized result header handoff. pub const KERNEL_RESULT_ADDR: u32 = 0x100; +/// User VA base for call arguments placed just above the program window. +pub(crate) const CALL_ARGS_PAGE_BASE: u32 = PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES as u32; /// User VA where the "to" address bytes are copied for program calls. -pub(crate) const TO_PTR_ADDR: u32 = 0x120; +pub(crate) const TO_PTR_ADDR: u32 = CALL_ARGS_PAGE_BASE + 0x100; /// User VA where the "from" address bytes are copied for program calls. pub(crate) const FROM_PTR_ADDR: u32 = TO_PTR_ADDR + ADDRESS_LEN as u32; -/// User VA base for the input buffer in the program heap window. -pub(crate) const INPUT_BASE_ADDR: u32 = HEAP_START_ADDR as u32; +/// User VA base for the input buffer in the call-args page. +pub(crate) const INPUT_BASE_ADDR: u32 = FROM_PTR_ADDR + ADDRESS_LEN as u32; // ============================================ // Task Scheduling and Bookkeeping @@ -189,3 +202,7 @@ pub static ROOT_PPN: Global = Global::new(0); pub static PAGE_ALLOC: Global> = Global::new(None); /// Kernel heap allocator instance. pub static KERNEL_HEAP: Global = Global::new(BumpAllocator::empty()); + +const fn align_up(val: usize, align: usize) -> usize { + (val + (align - 1)) & !(align - 1) +} diff --git a/crates/kernel/src/syscall/balance.rs b/crates/kernel/src/syscall/balance.rs index bd1caf3..579b67c 100644 --- a/crates/kernel/src/syscall/balance.rs +++ b/crates/kernel/src/syscall/balance.rs @@ -6,7 +6,7 @@ use state::State; use crate::global::{CURRENT_TASK, KERNEL_TASK_SLOT, STATE}; use crate::memory::page_allocator as mmu; use crate::syscall::alloc::sys_alloc; -use crate::syscall::storage::{current_root_ppn, read_user_bytes}; +use crate::syscall::storage::{current_task_root_ppn, read_user_bytes}; use crate::global::FROM_PTR_ADDR; pub(crate) fn sys_transfer(args: [u32; 6]) -> u32 { @@ -16,7 +16,7 @@ pub(crate) fn sys_transfer(args: [u32; 6]) -> u32 { return 1; } - let root_ppn = match current_root_ppn() { + let root_ppn = match current_task_root_ppn() { Some(root) => root, None => return 1, }; @@ -56,7 +56,7 @@ pub(crate) fn sys_balance(args: [u32; 6]) -> u32 { return 0; } - let root_ppn = match current_root_ppn() { + let root_ppn = match current_task_root_ppn() { Some(root) => root, None => return 0, }; diff --git a/crates/kernel/src/syscall/call_program.rs b/crates/kernel/src/syscall/call_program.rs index 9633cdd..4448d78 100644 --- a/crates/kernel/src/syscall/call_program.rs +++ b/crates/kernel/src/syscall/call_program.rs @@ -2,7 +2,7 @@ use clibc::logf; use types::{Address, ADDRESS_LEN}; use crate::global::{CURRENT_TASK, MAX_INPUT_LEN, TASKS}; -use crate::syscall::storage::{caller_address_matches, current_root_ppn, read_user_bytes}; +use crate::syscall::storage::{caller_address_matches, current_task_root_ppn, read_user_bytes}; use crate::syscall::SyscallContext; use crate::task::prep_program_task; use crate::user_program::with_program_image; @@ -21,7 +21,7 @@ pub(crate) fn sys_call_program(args: [u32; 6], ctx: &mut SyscallContext<'_>) -> return 0; } - let root_ppn = match current_root_ppn() { + let root_ppn = match current_task_root_ppn() { Some(root) => root, None => return 0, }; diff --git a/crates/kernel/src/syscall/fire_event.rs b/crates/kernel/src/syscall/fire_event.rs index d2a9c31..b3f0b0c 100644 --- a/crates/kernel/src/syscall/fire_event.rs +++ b/crates/kernel/src/syscall/fire_event.rs @@ -1,13 +1,13 @@ use clibc::logf; use crate::global::{CURRENT_TX, RECEIPTS}; -use crate::syscall::storage::{current_root_ppn, read_user_bytes}; +use crate::syscall::storage::{current_task_root_ppn, read_user_bytes}; pub(crate) fn sys_fire_event(args: [u32; 6]) -> u32 { let ptr = args[0]; let len = args[1] as usize; - let root_ppn = match current_root_ppn() { + let root_ppn = match current_task_root_ppn() { Some(root) => root, None => return 0, }; diff --git a/crates/kernel/src/syscall/storage.rs b/crates/kernel/src/syscall/storage.rs index cdb2f44..0841dd3 100644 --- a/crates/kernel/src/syscall/storage.rs +++ b/crates/kernel/src/syscall/storage.rs @@ -20,7 +20,7 @@ pub(crate) fn sys_storage_get(args: [u32; 6]) -> u32 { let domain_len = lens_packed & 0xffff; let key_len = lens_packed >> 16; - let root_ppn = match current_root_ppn() { + let root_ppn = match current_task_root_ppn() { Some(root) => root, None => return 0, }; @@ -111,7 +111,7 @@ pub(crate) fn sys_storage_set(args: [u32; 6]) -> u32 { let domain_len = lens_packed & 0xffff; let key_len = lens_packed >> 16; - let root_ppn = match current_root_ppn() { + let root_ppn = match current_task_root_ppn() { Some(root) => root, None => return 0, }; @@ -164,7 +164,7 @@ pub(crate) fn sys_storage_set(args: [u32; 6]) -> u32 { 0 } -pub(crate) fn current_root_ppn() -> Option { +pub(crate) fn current_task_root_ppn() -> Option { let current = unsafe { *CURRENT_TASK.get_mut() }; let tasks = unsafe { TASKS.get_mut() }; match tasks.get(current) { diff --git a/crates/kernel/src/task/mod.rs b/crates/kernel/src/task/mod.rs index 91d93ca..906ea1a 100644 --- a/crates/kernel/src/task/mod.rs +++ b/crates/kernel/src/task/mod.rs @@ -7,8 +7,8 @@ // - Map a fixed, contiguous user window starting at VA 0x0 that holds: // * Code/rodata (program bytes copied starting at VA 0x0; entry at `entry_off`) // * A user stack (STACK_BYTES) -// * A user heap (HEAP_BYTES) with input placed at INPUT_BASE_ADDR -// - Copy call arguments (to/from addresses + input buffer) into that window. +// * A user heap (HEAP_BYTES) with call args in a dedicated page at INPUT_BASE_ADDR +// - Copy call arguments (to/from addresses + input buffer) into that call-args page. // - Prepare a trapframe with PC/SP/args and transfer control to user code. // // Key pieces: @@ -21,7 +21,7 @@ // This keeps trap entry valid even when the current root is the user page table. // // prep_program_task(to, from, code, input, entry_off): -// 1) Allocate ASID and a fresh root PPN; map the user window with user_rwx perms. +// 1) Allocate ASID and a fresh root PPN; map the user window + call-args page. // 2) Copy program code starting at VA 0 (so section offsets are preserved), copy args (to/from/input). // 3) Map the trampoline page into the user root and mirror the same physical page // into the current kernel root; write trampoline code into it. @@ -48,7 +48,7 @@ // - We currently do not touch sstatus/mstatus or perform sfence.vma; add those // when modeling fuller privilege transitions. -use crate::global::{CODE_SIZE_LIMIT, NEXT_ASID, RO_DATA_SIZE_LIMIT}; +use crate::global::NEXT_ASID; pub mod task; pub mod prep; @@ -60,9 +60,8 @@ pub use prep::prep_program_task; pub use run::{kernel_run_task, run_task}; const PAGE_SIZE: usize = 4096; -const STACK_BYTES: usize = 0x4000; // 16 KiB user stack -pub const HEAP_BYTES: usize = 0x8000; // 32 KiB user heap -pub const PROGRAM_VA_BASE: u32 = 0x0; +const STACK_BYTES: usize = crate::global::STACK_BYTES; +pub const HEAP_BYTES: usize = crate::global::HEAP_BYTES; // Location of the page that hosts the satp-switch trampolines. Kept just past // the user window so it does not collide with program text/stack/heap. This VA // is mapped into both roots so satp can be switched without invalidating the @@ -72,15 +71,7 @@ pub const TRAMPOLINE_VA: u32 = const TRAP_TRAMPOLINE_OFFSET: usize = 0x10; // Offset for the trap-entry stub within the page. pub const TRAP_TRAMPOLINE_VA: u32 = TRAMPOLINE_VA + TRAP_TRAMPOLINE_OFFSET as u32; // stvec target for user-mode traps. -const fn align_up(val: usize, align: usize) -> usize { - (val + (align - 1)) & !(align - 1) -} - -/// Total mapped window for a program: code/rodata, stack, and heap. -pub const PROGRAM_WINDOW_BYTES: usize = align_up( - CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + STACK_BYTES + HEAP_BYTES, - PAGE_SIZE, -); +pub use crate::global::{PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES}; const REG_SP: usize = 2; const REG_RA: usize = 1; diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index b216d14..356eb45 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -1,11 +1,12 @@ use crate::{AddressSpace, Task}; use crate::global::{ - CODE_SIZE_LIMIT, CURRENT_TASK, FROM_PTR_ADDR, HEAP_START_ADDR, INPUT_BASE_ADDR, MAX_INPUT_LEN, - RO_DATA_SIZE_LIMIT, TO_PTR_ADDR, + CALL_ARGS_PAGE_BASE, CODE_SIZE_LIMIT, CURRENT_TASK, FROM_PTR_ADDR, HEAP_START_ADDR, + INPUT_BASE_ADDR, MAX_INPUT_LEN, RO_DATA_SIZE_LIMIT, TO_PTR_ADDR, }; use crate::memory::page_allocator as mmu; use clibc::{log, logf}; use types::address::Address; +use types::SV32_PAGE_SIZE; use super::{ alloc_asid, trampoline::map_trampoline_page, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, REG_A0, @@ -49,6 +50,13 @@ pub fn prep_program_task( PROGRAM_VA_BASE, window_end ); + let args_perms = mmu::PagePerms::new(true, false, false, true); + if !mmu::map_range_for_root(root_ppn, CALL_ARGS_PAGE_BASE, SV32_PAGE_SIZE, args_perms) { + panic!( + "launch_program: failed to map call-args page (root=0x{:x})", + root_ppn + ); + } let perms = mmu::PagePerms::user_rwx(); if !mmu::map_range_for_root(root_ppn, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { panic!("launch_program: mapping failed (root=0x{:x})", root_ppn); @@ -121,8 +129,7 @@ pub fn prep_program_task( let caller = unsafe { *CURRENT_TASK.get_mut() }; task.caller_task_id = Some(caller); // Set up initial trapframe. - let stack_top = PROGRAM_VA_BASE - .wrapping_add((CODE_SIZE_LIMIT + RO_DATA_SIZE_LIMIT + STACK_BYTES) as u32); + let stack_top = PROGRAM_VA_BASE.wrapping_add(PROGRAM_WINDOW_BYTES as u32); task.tf.pc = entry_va; task.tf.regs[REG_SP] = stack_top; task.tf.regs[REG_A0] = TO_PTR_ADDR; From f385f6811b960af0acf598f989195b15c4a947ee Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Fri, 2 Jan 2026 23:10:49 +0200 Subject: [PATCH 60/70] Document kernel design and update project overview - add kernel README with responsibilities, task lifecycle, and memory layout - include ASCII diagrams for task, trap, and memory flow - refresh top-level project structure and architecture overview --- README.md | 32 ++++---- crates/kernel/README.md | 175 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 crates/kernel/README.md diff --git a/README.md b/README.md index f36fe76..e49b321 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ Blockchain technology is revolutionizing how we think about trust, decentralizat ``` rust-vm/ ├── crates/ -│ ├── avm/ # Application Virtual Machine - main orchestrator +│ ├── bootloader/ # Guest bootloader and test harness integration +│ ├── kernel/ # Guest kernel (tasks, syscalls, memory layout) │ ├── compiler/ # Rust-to-bytecode compiler │ ├── examples/ # Smart contract examples and tutorials │ │ ├── README.md # 📖 [Detailed guide to all examples](crates/examples/README.md) @@ -119,6 +120,7 @@ rust-vm/ │ ├── clibc/ # Chain Libc smart contract runtime library │ ├── state/ # Blockchain state management │ ├── storage/ # Persistent storage system +│ ├── test-suite/ # End-to-end and integration tests │ ├── types/ # Common types and data structures │ └── vm/ # RISC-V virtual machine core │ └── tests/ # RISC-V compliance tests @@ -131,22 +133,18 @@ rust-vm/ ### 🏛️ **Architecture Overview** ``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Transaction │ │ Smart Contract│ │ RISC-V VM │ -│ Processing │───▶│ Execution │───▶│ Core │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ State Mgmt │ │ Memory Mgmt │ │ Instruction │ -│ (Accounts) │ │ (Pages) │ │ Decoder │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Storage │ │ Context Stack │ │ CPU & Regs │ -│ (Persistent) │ │ (Call Chain) │ │ (Execution) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ +┌───────────────┐ ┌───────────────────┐ ┌──────────────────┐ +│ Bootloader │─▶│ Kernel │─▶│ User Tasks │ +│ (BootInfo, │ │ init, traps, │ │ (addr space, │ +│ VM setup) │ │ syscalls) │ │ trapframe) │ +└───────────────┘ └─────────┬─────────┘ └────────┬─────────┘ + │ │ + v v + ┌───────────────────┐ ┌──────────────────┐ + │ State + Storage │ │ RISC-V VM │ + │ (accounts, │ │ (CPU + decoder) │ + │ receipts) │ └──────────────────┘ + └───────────────────┘ ``` --- diff --git a/crates/kernel/README.md b/crates/kernel/README.md new file mode 100644 index 0000000..c698d2c --- /dev/null +++ b/crates/kernel/README.md @@ -0,0 +1,175 @@ +# Kernel crate + +This crate implements the guest kernel that runs inside the VM. It is +responsible for bootstrapping the system, managing address spaces, launching +user programs, handling traps and syscalls, and returning receipts/results to +the bootloader. + +## High-level responsibilities + +- Boot-time initialization from `BootInfo` (page tables, heap, state). +- Page allocation and page-table mapping. +- Task creation and scheduling (single-threaded, cooperative). +- Trap entry/exit and syscall dispatch. +- User program loading, execution, and result collection. +- Kernel storage and receipt management. + +## Boot and initialization flow + +1) Bootloader writes `BootInfo` into guest memory and jumps to kernel entry. +2) `init_kernel` reads `BootInfo`, sets `ROOT_PPN`, initializes allocator + state, and sets up the kernel heap window. +3) Trap vector is installed and the kernel starts processing transaction + bundles. Each bundle is decoded and executed by creating tasks. + +Key files: +- `src/init.rs`: kernel init, boot info parsing. +- `src/memory/`: allocator and page table helpers. +- `src/trap/`: trap entry and syscall dispatch. +- `src/task/`: task creation, scheduling, and context switch. + +## Task model + +A task represents an isolated user program execution with its own: +- Address space (root page table, ASID). +- Trapframe (PC, SP, argument registers). +- User heap pointer. +- Caller info (task id). + +Core types: +- `Task` (`src/task/task.rs`): task state and context. +- `AddressSpace` (`src/task/task.rs`): root PPN + user VA window info. + +### Task lifecycle + +1) **Creation** + - A fresh ASID and root page table are allocated. + - The user program window is mapped with user permissions. + - A dedicated call-args page is mapped just above the user window. + - Program bytes are copied into the user window. + - Call arguments (to/from/input) are copied into the call-args page. + - The trapframe is initialized (PC, SP, A0..A3). + +2) **Run** + - `run_task` switches `satp` to the task root and jumps to user PC. + - Syscalls trap back into the kernel via the trampoline page. + +3) **Syscall handling** + - The trap handler switches to kernel context, decodes syscall number, + and dispatches to the appropriate handler in `src/syscall/`. + - Results are written into the task context and returned to user. + +4) **Completion** + - The user program exits or triggers a breakpoint return. + - The kernel collects the result payload and writes a receipt. + - The kernel switches back to the caller task or the kernel task. + +Task lifetime diagram: + +``` + +---------+ +-----------+ +-------------+ +------------+ + | created | ---> | runnable | ---> | running | ---> | completed | + +---------+ +-----------+ +-------------+ +------------+ + ^ | + | v + +-------------- syscall/trap ----------+ +``` + +## Memory layout + +The kernel uses a flat VA window (from `BootInfo`) to access its heap and +static memory. User programs run in a separate user VA window starting at 0. + +### User program VA window + +Defined in `src/global.rs`: + +- `PROGRAM_VA_BASE`: base of user mappings (0x0). +- `PROGRAM_WINDOW_BYTES`: total mapped user window size. +- `CODE_SIZE_LIMIT`: max code size. +- `RO_DATA_SIZE_LIMIT`: reserved rodata size. +- `HEAP_BYTES`: user heap size. +- `STACK_BYTES`: user stack size. +- `HEAP_START_ADDR`: heap base within the user window. + +The stack is placed at the end of the user window and grows downward: + +``` +low VA high VA +| code | rodata | heap .............. | stack (grows down) | +^ PROGRAM_VA_BASE ^ stack_base ^ stack_top +``` + +### Call-args page + +Call arguments (to/from addresses and input buffer) live in a dedicated page +mapped just above the user window: + +``` +CALL_ARGS_PAGE_BASE = PROGRAM_VA_BASE + PROGRAM_WINDOW_BYTES +TO_PTR_ADDR = CALL_ARGS_PAGE_BASE + 0x100 +FROM_PTR_ADDR = TO_PTR_ADDR + ADDRESS_LEN +INPUT_BASE_ADDR = FROM_PTR_ADDR + ADDRESS_LEN +``` + +This keeps call-args separate from user heap/stack and avoids corruption. +The call-args page is mapped as user-read only. + +User memory map (not to scale): + +``` +VA 0x00000000 + | code | rodata | heap .............. | stack | call-args page | + ^ PROGRAM_VA_BASE ^ stack_top ^ CALL_ARGS_PAGE_BASE +VA 0x00000000 + PROGRAM_WINDOW_BYTES (end of user window) +``` + +### Kernel heap + +The kernel heap is a bump allocator initialized from `BootInfo.heap_ptr` and +bounded by the kernel VA window (`va_base .. va_base + va_len`). If the kernel +heap exhausts, the allocator panics. + +## Trap and syscall flow + +1) User executes an `ecall` or trap instruction. +2) Control transfers to the trampoline page (`TRAP_TRAMPOLINE_VA`). +3) Kernel trap handler saves user state and dispatches syscalls. +4) Syscall handlers live in `src/syscall/` and may read user memory using + the task root page table. +5) Return values are placed in the trapframe and execution resumes in user. + +Trap cycle diagram: + +``` + user code + | + | ecall / fault + v + TRAP_TRAMPOLINE_VA ---> kernel trap handler ---> syscall handler + ^ | + | v + return to user <----------- update trapframe <---+ +``` + +## Storage and receipts + +Kernel storage is maintained in the global `State` object. Syscalls can read +and write key/value pairs. Transaction receipts are written as tasks complete +and returned to the bootloader. + +## Debugging notes + +- Stack/heap overlap bugs are common if `stack_top` is not placed at the end + of the user window. +- Call-args memory should be isolated from user heap/stack. +- Kernel heap exhaustion will panic in `src/memory/heap.rs`. + +## Relevant files + +- `src/global.rs`: global constants and kernel-wide state. +- `src/task/prep.rs`: program loading and trapframe setup. +- `src/task/run.rs`: context switch and run loop. +- `src/trap/mod.rs`: trap entry/exit and syscall dispatch. +- `src/syscall/`: syscall implementations. +- `src/memory/page_allocator.rs`: page allocation and mapping. From e70f43d9d13f00dcfe7cb54581c628422d64a235 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 07:48:53 +0200 Subject: [PATCH 61/70] removed claude settings --- .claude/settings.local.json | 42 ------------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index c0acb52..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(make test:*)", - "Bash(make:*)", - "Bash(cargo test:*)", - "Bash(cargo:*)", - "Bash(rm:*)", - "Bash(make:*)", - "Bash(grep:*)", - "Bash(timeout 10 cargo test -p examples --test program_exec)", - "Bash(RUST_BACKTRACE=0 cargo test -p examples --test program_exec --nocapture)", - "Bash(RUST_BACKTRACE=0 cargo test -p examples --test program_exec -- --nocapture)", - "Bash(timeout 30 make all)", - "Bash(timeout 30s cargo test -p examples)", - "Bash(mv:*)", - "WebSearch", - "Bash(find:*)", - "Bash(echo $?)", - "Bash(echo \"Exit code: $?\")", - "Bash(git add:*)", - "Bash(git push:*)", - "Bash(RUST_LOG=debug cargo test -p examples -- --test-threads=1 --nocapture)", - "Bash(timeout 5 cargo test -p examples test_entrypoint_function)", - "Bash(readelf:*)", - "Bash(objdump:*)", - "Bash(RUST_BACKTRACE=1 cargo test -p examples test_entrypoint_function)", - "Bash(rustc:*)", - "Read(//Users/alonmuroch/Desktop/**)", - "Bash(timeout 5 cargo test -p examples test_entrypoint_function -- --nocapture)", - "Bash(git commit:*)", - "Bash(utils/binary_comparison/target/release/binary_comparison:*)", - "Bash(RUST_LOG=debug ./target/debug/deps/program_exec-7efd2a542591705b --nocapture)", - "Bash(./target/release/binary_comparison:*)", - "Read(//private/tmp/**)", - "Read(//Users/alonmuroch/**)", - "Bash(timeout 10 make all)", - "Read(//tmp/**)" - ], - "deny": [] - } -} \ No newline at end of file From a2379edd298e50493802c9b8c7e3db49ce3f12fc Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 08:55:58 +0200 Subject: [PATCH 62/70] WIP aTester: add kernel test harness and AVM runner Add aTester crate with AVM runner, suite runner, and kernel test harness that builds kernel bins and prints VM logs. Wire kernel test ABI (input bytes + TestResults) with results struct and test utils init. Expose init_boot_info for test init path and add kernel_first_test bin. Add root Makefile kernel target and route run_examples through it. Include target spec fix and dependency updates in workspace and lockfile. --- Cargo.lock | 49 ++++- Cargo.toml | 1 + Makefile | 16 +- aTester/Cargo.toml | 13 ++ aTester/README.md | 13 ++ aTester/src/arch.rs | 49 +++++ aTester/src/lib.rs | 9 + aTester/src/runners/avm.rs | 263 ++++++++++++++++++++++++++ aTester/src/runners/mod.rs | 3 + aTester/src/suite.rs | 66 +++++++ aTester/src/types.rs | 21 ++ aTester/tests/kernel.rs | 148 +++++++++++++++ crates/kernel/Cargo.toml | 9 + crates/kernel/src/init.rs | 2 +- crates/kernel/src/tests/first_test.rs | 34 ++++ crates/kernel/src/tests/results.rs | 29 +++ crates/kernel/src/tests/utils.rs | 20 ++ 17 files changed, 737 insertions(+), 8 deletions(-) create mode 100644 aTester/Cargo.toml create mode 100644 aTester/README.md create mode 100644 aTester/src/arch.rs create mode 100644 aTester/src/lib.rs create mode 100644 aTester/src/runners/avm.rs create mode 100644 aTester/src/runners/mod.rs create mode 100644 aTester/src/suite.rs create mode 100644 aTester/src/types.rs create mode 100644 aTester/tests/kernel.rs create mode 100644 crates/kernel/src/tests/first_test.rs create mode 100644 crates/kernel/src/tests/results.rs create mode 100644 crates/kernel/src/tests/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 8f4da61..7a42c69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "a_tests" +version = "0.1.0" +dependencies = [ + "compiler", + "goblin 0.8.2", + "types", + "vm", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -29,7 +39,7 @@ version = "0.1.0" dependencies = [ "clibc", "compiler", - "goblin", + "goblin 0.10.0", "state", "types", "vm", @@ -53,7 +63,7 @@ dependencies = [ name = "compiler" version = "0.1.0" dependencies = [ - "goblin", + "goblin 0.10.0", ] [[package]] @@ -184,6 +194,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll 0.12.0", +] + [[package]] name = "goblin" version = "0.10.0" @@ -192,7 +213,7 @@ checksum = "0e961b33649994dcf69303af6b3a332c1228549e604d455d61ec5d2ab5e68d3a" dependencies = [ "log", "plain", - "scroll", + "scroll 0.13.0", ] [[package]] @@ -322,13 +343,33 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive 0.12.1", +] + [[package]] name = "scroll" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" dependencies = [ - "scroll_derive", + "scroll_derive 0.13.0", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 58ee6f2..56c0260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/storage", "crates/types", "crates/vm", + "aTester", ] [profile.release] diff --git a/Makefile b/Makefile index 1366803..4962f1d 100644 --- a/Makefile +++ b/Makefile @@ -3,17 +3,27 @@ # Nightly cargo for avm32 builds (used for kernel ELF and examples). CARGO_NIGHTLY ?= cargo +nightly-aarch64-apple-darwin AVM32 := $(CARGO_NIGHTLY) run -p compiler --bin avm32 -- +KERNEL_MANIFEST := crates/kernel/Cargo.toml +KERNEL_OUT_DIR := crates/bootloader/bin +KERNEL_BINS := $(shell awk '/\[\[bin\]\]/{inbin=1;next} inbin && /name =/{gsub(/"/,"",$$3); print $$3; inbin=0}' $(KERNEL_MANIFEST)) +KERNEL_TEST_BINS := $(filter-out kernel,$(KERNEL_BINS)) all: clean program examples test utils summary .PHONY: run_examples +.PHONY: kernel + +kernel: + @echo "=== Building kernel ELF ===" + @mkdir -p $(KERNEL_OUT_DIR) + @$(AVM32) all --bin kernel --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/main.rs + @echo "=== Building kernel test ELFs ===" + @$(foreach bin,$(KERNEL_TEST_BINS),$(AVM32) all --bin $(bin) --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/tests/$(patsubst kernel_%,%,$(bin)).rs;) run_examples: @echo "=== Building example programs ===" RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples - @echo "=== Building kernel ELF ===" - @mkdir -p crates/bootloader/bin - @$(AVM32) all --bin kernel --manifest-path crates/kernel/Cargo.toml --features guest_kernel --out-dir crates/bootloader/bin --src crates/kernel/src/main.rs + @$(MAKE) kernel @echo "=== Running example crate tests ===" cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../bootloader/bin/kernel.elf" cargo test --test examples_test -- --nocapture @echo "=== Example programs build and tests complete ===" diff --git a/aTester/Cargo.toml b/aTester/Cargo.toml new file mode 100644 index 0000000..171cfac --- /dev/null +++ b/aTester/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "a_tests" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib.rs" + +[dependencies] +compiler = { path = "../crates/compiler" } +goblin = "0.8" +types = { path = "../crates/types" } +vm = { path = "../crates/vm" } diff --git a/aTester/README.md b/aTester/README.md new file mode 100644 index 0000000..7107101 --- /dev/null +++ b/aTester/README.md @@ -0,0 +1,13 @@ +# aTests + +Architecture-aware test suite scaffold. + +Intent: +- Run ELF binaries on a supplied architecture runner (VM, QEMU, etc). +- Allow multiple test kinds with different assertion logic. +- Keep the runner abstract and pluggable. + +Current shape: +- `ArchRunner`: runs an ELF on an architecture and returns logs/exit code. +- `TestEvaluator`: evaluates a `RunResult` based on `TestCase` kind. +- `Suite`: runs a list of test cases through a runner. diff --git a/aTester/src/arch.rs b/aTester/src/arch.rs new file mode 100644 index 0000000..b43dc66 --- /dev/null +++ b/aTester/src/arch.rs @@ -0,0 +1,49 @@ +use std::fmt; + +use crate::types::{ElfTarget, RunOptions}; + +#[derive(Debug, Clone)] +pub struct RunResult { + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} + +#[derive(Debug)] +pub struct RunError { + pub message: String, +} + +impl fmt::Display for RunError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for RunError {} + +pub trait ArchRunner { + fn name(&self) -> &str; + fn run(&self, elf: &ElfTarget, options: &RunOptions) -> Result; +} + +pub struct ArchRegistry { + runners: Vec>, +} + +impl ArchRegistry { + pub fn new() -> Self { + Self { runners: Vec::new() } + } + + pub fn register(&mut self, runner: Box) { + self.runners.push(runner); + } + + pub fn get(&self, name: &str) -> Option<&dyn ArchRunner> { + self.runners + .iter() + .find(|runner| runner.name() == name) + .map(|runner| runner.as_ref()) + } +} diff --git a/aTester/src/lib.rs b/aTester/src/lib.rs new file mode 100644 index 0000000..cb7db94 --- /dev/null +++ b/aTester/src/lib.rs @@ -0,0 +1,9 @@ +mod arch; +mod runners; +mod suite; +mod types; + +pub use arch::{ArchRegistry, ArchRunner, RunError, RunResult}; +pub use runners::AvmRunner; +pub use suite::{Suite, TestCase, TestKind, TestReport, TestEvaluator}; +pub use types::{ElfTarget, RunOptions, TestOutcome}; diff --git a/aTester/src/runners/avm.rs b/aTester/src/runners/avm.rs new file mode 100644 index 0000000..ae745cc --- /dev/null +++ b/aTester/src/runners/avm.rs @@ -0,0 +1,263 @@ +use std::cell::{Cell, RefCell}; +use std::fmt::Write as FmtWrite; +use std::fs; +use std::mem; +use std::rc::Rc; + +use compiler::elf::parse_elf_from_bytes; +use goblin::elf::Elf; +use types::boot::BootInfo; +use types::SV32_DIRECT_MAP_BASE; +use vm::memory::{API, MMU, Perms, Sv32Memory, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SIZE}; +use vm::registers::Register; +use vm::vm::VM; + +use crate::arch::{ArchRunner, RunError, RunResult}; +use crate::types::{ElfTarget, RunOptions}; + +pub struct AvmRunner; + +impl AvmRunner { + pub fn new() -> Self { + Self + } +} + +impl ArchRunner for AvmRunner { + fn name(&self) -> &str { + "avm" + } + + fn run(&self, elf: &ElfTarget, options: &RunOptions) -> Result { + let elf_bytes = fs::read(&elf.path).map_err(|e| RunError { + message: format!("failed to read elf {}: {e}", elf.path.display()), + })?; + + let total_size = options.vm_memory_size.unwrap_or(16 * 1024 * 1024); + let memory = Rc::new(Sv32Memory::new(total_size, PAGE_SIZE)); + let heap_ptr = Rc::new(Cell::new(0u32)); + let entry_point = load_kernel(&elf_bytes, &memory, heap_ptr.as_ref())?; + + let input_ptr = if options.input.is_empty() { + 0 + } else { + alloc_on_heap(memory.as_ref(), heap_ptr.as_ref(), &options.input) + }; + let boot_info_ptr = place_boot_info(memory.as_ref(), heap_ptr.as_ref(), total_size)?; + + let mut vm = VM::new(memory.clone()); + vm.set_reg_u32(Register::Sp, KERNEL_STACK_TOP); + vm.cpu.verbose = options.verbose; + + let writer = Rc::new(RefCell::new(StringWriter::default())); + vm.cpu.set_verbose_writer(writer.clone()); + vm.cpu.pc = entry_point; + vm.set_reg_u32(Register::A0, input_ptr); + vm.set_reg_u32(Register::A1, options.input.len() as u32); + vm.set_reg_u32(Register::A2, boot_info_ptr); + + vm.raw_run(); + + let stdout = writer.borrow().buffer.clone(); + let results = read_test_results(memory.as_ref())?; + let exit_code = if results.status == 0 { 0 } else { 1 }; + let stderr = if results.status == 0 { + String::new() + } else { + format!("test failed: {}", results.detail) + }; + + Ok(RunResult { + exit_code, + stdout, + stderr, + }) + } +} + +const KERNEL_WINDOW_BYTES: usize = 4 * 1024 * 1024; +const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; +const TEST_RESULTS_ADDR: u32 = 0x0003_f000; + +#[repr(C)] +struct TestResults { + status: u32, + detail: u32, +} + +fn read_test_results(memory: &Sv32Memory) -> Result { + let start = VirtualAddress(TEST_RESULTS_ADDR); + let end = start + .checked_add(mem::size_of::() as u32) + .ok_or_else(|| RunError { + message: "test results overflow".to_string(), + })?; + let slice = memory + .mem_slice(start, end) + .ok_or_else(|| RunError { + message: "test results not found".to_string(), + })?; + let bytes = slice.as_ref(); + if bytes.len() < mem::size_of::() { + return Err(RunError { + message: "test results truncated".to_string(), + }); + } + let status = u32::from_le_bytes(bytes[0..4].try_into().unwrap()); + let detail = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); + Ok(TestResults { status, detail }) +} + +fn load_kernel( + elf_bytes: &[u8], + memory: &Rc, + heap_ptr: &Cell, +) -> Result { + let elf = parse_elf_from_bytes(elf_bytes).map_err(|e| RunError { + message: format!("failed to parse kernel elf: {e}"), + })?; + let entry_point = Elf::parse(elf_bytes) + .map_err(|e| RunError { + message: format!("failed to parse entry point: {e}"), + })? + .entry as u32; + + let (code, code_base) = elf + .get_flat_code() + .ok_or_else(|| RunError { + message: "kernel elf missing .text".to_string(), + })?; + let (rodata, ro_base) = elf.get_flat_rodata().unwrap_or((Vec::new(), code_base)); + let (bss, bss_base) = elf.get_flat_bss().unwrap_or((Vec::new(), code_base)); + + let mut min_base = core::cmp::min(code_base, ro_base) as usize; + if !bss.is_empty() { + min_base = core::cmp::min(min_base, bss_base as usize); + } + let code_end = (code_base + code.len() as u64) as usize; + let ro_end = (ro_base + rodata.len() as u64) as usize; + let mut image_end = core::cmp::max(code_end, ro_end); + if !bss.is_empty() { + let bss_end = bss_base + .checked_add(bss.len() as u64) + .ok_or_else(|| RunError { + message: "bss end overflow".to_string(), + })? as usize; + image_end = core::cmp::max(image_end, bss_end); + } + let image_size = image_end + .checked_sub(min_base) + .ok_or_else(|| RunError { + message: "invalid image size".to_string(), + })?; + + if image_end > memory.size() { + return Err(RunError { + message: format!( + "elf image does not fit in mapped memory (need {}, have {})", + image_end, + memory.size() + ), + }); + } + if KERNEL_WINDOW_BYTES > memory.size() { + return Err(RunError { + message: format!( + "kernel window exceeds physical memory (need {}, have {})", + KERNEL_WINDOW_BYTES, + memory.size() + ), + }); + } + + let mut image = vec![0u8; image_size]; + let code_off = (code_base as usize).saturating_sub(min_base); + image[code_off..code_off + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + let ro_off = (ro_base as usize).saturating_sub(min_base); + image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); + } + if !bss.is_empty() { + let bss_off = (bss_base as usize).saturating_sub(min_base); + image[bss_off..bss_off + bss.len()].copy_from_slice(&bss); + } + + memory.map_range(VirtualAddress(0), KERNEL_WINDOW_BYTES, Perms::rwx_kernel()); + memory.write_bytes(VirtualAddress(min_base as u32), &image); + + let heap_start = ((image_end + HEAP_PTR_OFFSET as usize + 7) & !7) as u32; + heap_ptr.set(heap_start); + + let mapped = memory.map_physical_range( + VirtualAddress(SV32_DIRECT_MAP_BASE), + 0, + memory.size(), + Perms::rw_kernel(), + ); + if !mapped { + return Err(RunError { + message: "failed to map kernel direct physical window".to_string(), + }); + } + + Ok(entry_point) +} + +fn place_boot_info(memory: &Sv32Memory, heap_ptr: &Cell, memory_size: usize) -> Result { + let heap_start = ensure_heap_ptr(heap_ptr); + let aligned_heap = (heap_start + 7) & !7; + let boot_info_size = mem::size_of::() as u32; + let next_heap = aligned_heap + .checked_add(boot_info_size) + .and_then(|v| v.checked_add(HEAP_PTR_OFFSET)) + .ok_or_else(|| RunError { + message: "boot info heap pointer overflow".to_string(), + })?; + let boot_info = BootInfo::new( + memory.current_root() as u32, + KERNEL_STACK_TOP, + next_heap, + memory_size as u32, + memory.next_free_ppn() as u32, + 0, + KERNEL_WINDOW_BYTES as u32, + ); + let bytes = unsafe { + core::slice::from_raw_parts( + &boot_info as *const BootInfo as *const u8, + mem::size_of::(), + ) + }; + let addr = alloc_on_heap(memory, heap_ptr, bytes); + Ok(addr) +} + +fn alloc_on_heap(memory: &Sv32Memory, heap_ptr: &Cell, data: &[u8]) -> u32 { + let addr = ensure_heap_ptr(heap_ptr); + memory.write_bytes(VirtualAddress(addr), data); + let next = (addr + data.len() as u32 + HEAP_PTR_OFFSET + 7) & !7; + heap_ptr.set(next); + addr +} + +fn ensure_heap_ptr(heap_ptr: &Cell) -> u32 { + let current = heap_ptr.get(); + if current == 0 { + heap_ptr.set(HEAP_PTR_OFFSET); + HEAP_PTR_OFFSET + } else { + current + } +} + +#[derive(Default)] +struct StringWriter { + buffer: String, +} + +impl FmtWrite for StringWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + self.buffer.push_str(s); + Ok(()) + } +} diff --git a/aTester/src/runners/mod.rs b/aTester/src/runners/mod.rs new file mode 100644 index 0000000..905f9c5 --- /dev/null +++ b/aTester/src/runners/mod.rs @@ -0,0 +1,3 @@ +mod avm; + +pub use avm::AvmRunner; diff --git a/aTester/src/suite.rs b/aTester/src/suite.rs new file mode 100644 index 0000000..60fc6bd --- /dev/null +++ b/aTester/src/suite.rs @@ -0,0 +1,66 @@ +use std::path::PathBuf; + +use crate::arch::{ArchRunner, RunResult}; +use crate::types::{ElfTarget, RunOptions, TestOutcome}; + +#[derive(Debug, Clone)] +pub enum TestKind { + Smoke, + OutputMatch, + InstructionTrace, +} + +#[derive(Debug, Clone)] +pub struct TestCase { + pub name: String, + pub kind: TestKind, + pub elf: PathBuf, + pub options: RunOptions, +} + +#[derive(Debug, Clone)] +pub struct TestReport { + pub name: String, + pub outcome: TestOutcome, + pub runner: String, + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} + +pub trait TestEvaluator { + fn evaluate(&self, case: &TestCase, result: &RunResult) -> TestOutcome; +} + +pub struct Suite<'a> { + pub name: String, + pub cases: Vec, + pub evaluator: &'a dyn TestEvaluator, +} + +impl<'a> Suite<'a> { + pub fn run(&self, runner: &dyn ArchRunner) -> Vec { + let mut reports = Vec::new(); + for case in &self.cases { + let elf = ElfTarget { + path: case.elf.clone(), + }; + let (outcome, exit_code, stdout, stderr) = match runner.run(&elf, &case.options) { + Ok(result) => { + let outcome = self.evaluator.evaluate(case, &result); + (outcome, result.exit_code, result.stdout, result.stderr) + } + Err(err) => (TestOutcome::Failed(err.message.clone()), -1, String::new(), err.message), + }; + reports.push(TestReport { + name: case.name.clone(), + outcome, + runner: runner.name().to_string(), + exit_code, + stdout, + stderr, + }); + } + reports + } +} diff --git a/aTester/src/types.rs b/aTester/src/types.rs new file mode 100644 index 0000000..38ce1a2 --- /dev/null +++ b/aTester/src/types.rs @@ -0,0 +1,21 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct ElfTarget { + pub path: PathBuf, +} + +#[derive(Debug, Clone, Default)] +pub struct RunOptions { + pub timeout_ms: Option, + pub vm_memory_size: Option, + pub verbose: bool, + pub input: Vec, +} + +#[derive(Debug, Clone)] +pub enum TestOutcome { + Passed, + Failed(String), + Skipped(String), +} diff --git a/aTester/tests/kernel.rs b/aTester/tests/kernel.rs new file mode 100644 index 0000000..88419ed --- /dev/null +++ b/aTester/tests/kernel.rs @@ -0,0 +1,148 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use a_tests::{AvmRunner, RunOptions, Suite, TestCase, TestEvaluator, TestKind, TestOutcome}; + +struct ExitCodeEvaluator; + +impl TestEvaluator for ExitCodeEvaluator { + fn evaluate(&self, case: &TestCase, result: &a_tests::RunResult) -> TestOutcome { + if result.exit_code == 0 { + TestOutcome::Passed + } else { + let detail = if result.stderr.is_empty() { + format!("{} failed with exit code {}", case.name, result.exit_code) + } else { + result.stderr.clone() + }; + TestOutcome::Failed(detail) + } + } +} + +#[test] +fn kernel_tests() { + build_kernel().expect("failed to build kernel test bins"); + let bins = kernel_bins().expect("failed to discover kernel bins"); + if bins.is_empty() { + panic!("no kernel test bins found"); + } + + let target_dir = kernel_elf_dir(); + let cases = bins + .into_iter() + .map(|name| TestCase { + name: name.clone(), + kind: TestKind::Smoke, + elf: target_dir.join(format!("{name}.elf")), + options: RunOptions { + timeout_ms: None, + vm_memory_size: None, + verbose: false, + input: Vec::new(), + }, + }) + .collect::>(); + + let evaluator = ExitCodeEvaluator; + let suite = Suite { + name: "kernel_tests".to_string(), + cases, + evaluator: &evaluator, + }; + + let runner = AvmRunner::new(); + for case in &suite.cases { + println!("running kernel test: {}", case.name); + } + let reports = suite.run(&runner); + + for report in &reports { + if !report.stdout.is_empty() { + println!("--- {} stdout ---\n{}", report.name, report.stdout); + } + if !report.stderr.is_empty() { + eprintln!("--- {} stderr ---\n{}", report.name, report.stderr); + } + } + + let failures: Vec<_> = reports + .iter() + .filter(|report| matches!(report.outcome, TestOutcome::Failed(_))) + .collect(); + + if !failures.is_empty() { + let mut details = String::new(); + for report in failures { + if let TestOutcome::Failed(detail) = &report.outcome { + details.push_str(&format!("{}: {}\n", report.name, detail)); + } + } + panic!("kernel test failures:\n{}", details); + } +} + +fn kernel_bins() -> Result, String> { + let manifest_path = workspace_root().join("crates/kernel/Cargo.toml"); + let contents = fs::read_to_string(&manifest_path) + .map_err(|e| format!("failed to read kernel Cargo.toml: {e}"))?; + + let mut bins = Vec::new(); + let mut current_name: Option = None; + + for line in contents.lines() { + let line = line.trim(); + if line == "[[bin]]" { + if let Some(name) = current_name.take() { + if name != "kernel" { + bins.push(name); + } + } + continue; + } + if let Some(name) = line.strip_prefix("name = ") { + let name = name.trim().trim_matches('"').to_string(); + current_name = Some(name); + } + } + + if let Some(name) = current_name { + if name != "kernel" { + bins.push(name); + } + } + + if bins.is_empty() { + return Err("no [[bin]] entries found".to_string()); + } + + Ok(bins) +} + +fn kernel_elf_dir() -> PathBuf { + std::env::var("KERNEL_ELF_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_root().join("crates/bootloader/bin")) +} + +fn build_kernel() -> Result<(), String> { + let status = std::process::Command::new("make") + .args(["kernel"]) + .current_dir(workspace_root()) + .status() + .map_err(|e| format!("failed to spawn kernel make: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("kernel build failed with status: {status}")) + } +} + +fn workspace_root() -> PathBuf { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .map(PathBuf::from) + .expect("missing workspace root") +} diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index b13e8f6..3d7a85a 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -19,3 +19,12 @@ state = { path = "../state" } name = "kernel" path = "src/main.rs" required-features = ["guest_kernel"] + +##################### +# Kernel Tests +##################### + +[[bin]] +name = "kernel_first_test" +path = "src/tests/first_test.rs" +required-features = ["guest_kernel"] diff --git a/crates/kernel/src/init.rs b/crates/kernel/src/init.rs index 0d36594..0e6b31c 100644 --- a/crates/kernel/src/init.rs +++ b/crates/kernel/src/init.rs @@ -41,7 +41,7 @@ fn init_state(state_ptr: *const u8, state_len: usize) { } } -fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { +pub(crate) fn init_boot_info(boot_info: Option<&BootInfo>) -> Option<&BootInfo> { logf!( "init_boot_info: boot_info_ptr=0x%x", boot_info diff --git a/crates/kernel/src/tests/first_test.rs b/crates/kernel/src/tests/first_test.rs new file mode 100644 index 0000000..dc57d51 --- /dev/null +++ b/crates/kernel/src/tests/first_test.rs @@ -0,0 +1,34 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +use core::slice; +use clibc::log; +use kernel::BootInfo; + +mod utils; +mod results; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel test boot"); + utils::init_test_kernel(boot_info_ptr); + + let input = unsafe { slice::from_raw_parts(input_ptr, input_len) }; + clibc::logf!("kernel test input len: %d", input.len() as u32); + log!("kernel test log-only"); + + unsafe { results::write_results(results::TestResults::pass(0)) }; + halt(); +} + +#[inline(never)] +fn halt() -> ! { + unsafe { core::arch::asm!("ebreak") }; + loop {} +} diff --git a/crates/kernel/src/tests/results.rs b/crates/kernel/src/tests/results.rs new file mode 100644 index 0000000..08f6cf4 --- /dev/null +++ b/crates/kernel/src/tests/results.rs @@ -0,0 +1,29 @@ +#[repr(C)] +#[derive(Clone, Copy)] +pub struct TestResults { + pub status: u32, + pub detail: u32, +} + +pub const TEST_RESULTS_ADDR: u32 = 0x0003_f000; + +impl TestResults { + pub const fn pass(detail: u32) -> Self { + Self { + status: 0, + detail, + } + } + + pub const fn fail(detail: u32) -> Self { + Self { + status: 1, + detail, + } + } +} + +pub unsafe fn write_results(results: TestResults) { + let ptr = TEST_RESULTS_ADDR as *mut TestResults; + ptr.write_volatile(results); +} diff --git a/crates/kernel/src/tests/utils.rs b/crates/kernel/src/tests/utils.rs new file mode 100644 index 0000000..296bab0 --- /dev/null +++ b/crates/kernel/src/tests/utils.rs @@ -0,0 +1,20 @@ +use clibc::log; +use kernel::memory::{heap, page_allocator}; +use kernel::{trap, BootInfo}; + +#[path = "../init.rs"] +mod init; + +pub fn init_test_kernel(boot_info_ptr: *const BootInfo) { + let boot_info = unsafe { boot_info_ptr.as_ref() }; + if let Some(info) = init::init_boot_info(boot_info) { + unsafe { + page_allocator::init(info); + heap::init(info.heap_ptr, info.va_base, info.va_len); + } + trap::init_trap_vector(info.kstack_top); + } else { + panic!("init_test_kernel: missing boot info"); + } + log!("kernel initialized"); +} From 0b08ecdc0aac52e7476d2ab054c6b68b3238b7db Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 15:27:18 +0200 Subject: [PATCH 63/70] WIP kernel memory tests --- crates/kernel/Cargo.toml | 10 ++ crates/kernel/src/memory/heap.rs | 3 + crates/kernel/src/memory/page_allocator.rs | 64 +++++++ crates/kernel/src/tests/first_test.rs | 12 +- crates/kernel/src/tests/mem_alloc_test.rs | 77 ++++++++ crates/kernel/src/tests/mem_map_test.rs | 196 +++++++++++++++++++++ crates/kernel/src/tests/results.rs | 4 +- crates/kernel/src/tests/utils.rs | 23 ++- crates/types/src/mmu.rs | 4 + 9 files changed, 381 insertions(+), 12 deletions(-) create mode 100644 crates/kernel/src/tests/mem_alloc_test.rs create mode 100644 crates/kernel/src/tests/mem_map_test.rs diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 3d7a85a..45c87cc 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -28,3 +28,13 @@ required-features = ["guest_kernel"] name = "kernel_first_test" path = "src/tests/first_test.rs" required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_mem_alloc_test" +path = "src/tests/mem_alloc_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_mem_map_test" +path = "src/tests/mem_map_test.rs" +required-features = ["guest_kernel"] diff --git a/crates/kernel/src/memory/heap.rs b/crates/kernel/src/memory/heap.rs index bb0dea6..ad68dc2 100644 --- a/crates/kernel/src/memory/heap.rs +++ b/crates/kernel/src/memory/heap.rs @@ -51,6 +51,9 @@ pub fn alloc(size: usize, align: usize) -> Option<*mut u8> { } } +/// Deallocate a kernel buffer. Bump allocator does not reclaim memory yet. +pub fn dealloc(_ptr: *mut u8, _size: usize, _align: usize) {} + fn align_up(value: usize, align: usize) -> Option { let mask = align - 1; value.checked_add(mask).map(|v| v & !mask) diff --git a/crates/kernel/src/memory/page_allocator.rs b/crates/kernel/src/memory/page_allocator.rs index 9ed9d5b..8171a90 100644 --- a/crates/kernel/src/memory/page_allocator.rs +++ b/crates/kernel/src/memory/page_allocator.rs @@ -252,6 +252,28 @@ pub fn translate(root_ppn: u32, va: u32) -> Option { ppn.checked_mul(PAGE_SIZE)?.checked_add(offset) } +fn leaf_pte(root_ppn: u32, va: u32) -> Option { + let vpn1 = (va >> 22) & SV32_VPN_MASK; + let vpn0 = (va >> 12) & SV32_VPN_MASK; + + let l1_base = (root_ppn as usize) + .checked_mul(PAGE_SIZE)?; + let l1_addr = l1_base + vpn1 as usize * core::mem::size_of::(); + let l1_pte = read_pte(l1_addr)?; + if l1_pte & SV32_PTE_V == 0 || l1_pte & (SV32_PTE_R | SV32_PTE_W | SV32_PTE_X) != 0 { + return None; + } + + let l2_base = ((l1_pte >> 10) as usize) + .checked_mul(PAGE_SIZE)?; + let l2_addr = l2_base + vpn0 as usize * core::mem::size_of::(); + let l2_pte = read_pte(l2_addr)?; + if l2_pte & SV32_PTE_V == 0 { + return None; + } + Some(l2_pte) +} + /// Peek a 32-bit value at a VA in a given root using the direct-map window. pub fn peek_word(root_ppn: u32, va: u32) -> Option { let phys = translate(root_ppn, va)?; @@ -292,6 +314,48 @@ pub fn copy(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { true } +/// Copy data into a user VA range, failing if any page is not user-writable. +pub fn copy_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { + if data.is_empty() { + return true; + } + let mut remaining = data.len(); + let mut src_off = 0usize; + let mut va = va_start; + while remaining > 0 { + let pte = match leaf_pte(root_ppn, va) { + Some(p) => p, + None => return false, + }; + let is_user = (pte & SV32_PTE_U) != 0; + let can_write = (pte & SV32_PTE_W) != 0; + if !is_user || !can_write { + return false; + } + let phys = match translate(root_ppn, va) { + Some(p) => p, + None => return false, + }; + let page_off = (va as usize) & (PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, PAGE_SIZE - page_off); + let dst = match direct_map_addr(phys) { + Some(v) => v, + None => return false, + }; + unsafe { + ptr::copy_nonoverlapping( + data.as_ptr().add(src_off), + dst as *mut u8, + to_copy, + ); + } + remaining -= to_copy; + src_off += to_copy; + va = va.wrapping_add(to_copy as u32); + } + true +} + /// Sv32 page-table accessor that routes PTE traffic through the kernel's direct map. struct KernelMapper<'a> { alloc: *mut PageAllocator, diff --git a/crates/kernel/src/tests/first_test.rs b/crates/kernel/src/tests/first_test.rs index dc57d51..9dd9c7b 100644 --- a/crates/kernel/src/tests/first_test.rs +++ b/crates/kernel/src/tests/first_test.rs @@ -3,6 +3,7 @@ extern crate alloc; +// Basic smoke test: init kernel test harness and emit logs. use core::slice; use clibc::log; use kernel::BootInfo; @@ -17,18 +18,11 @@ pub extern "C" fn _start( boot_info_ptr: *const BootInfo, ) { log!("kernel test boot"); - utils::init_test_kernel(boot_info_ptr); + let _info = utils::init_test_kernel(boot_info_ptr); let input = unsafe { slice::from_raw_parts(input_ptr, input_len) }; clibc::logf!("kernel test input len: %d", input.len() as u32); log!("kernel test log-only"); - unsafe { results::write_results(results::TestResults::pass(0)) }; - halt(); -} - -#[inline(never)] -fn halt() -> ! { - unsafe { core::arch::asm!("ebreak") }; - loop {} + utils::pass(); } diff --git a/crates/kernel/src/tests/mem_alloc_test.rs b/crates/kernel/src/tests/mem_alloc_test.rs new file mode 100644 index 0000000..652f921 --- /dev/null +++ b/crates/kernel/src/tests/mem_alloc_test.rs @@ -0,0 +1,77 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Memory allocation test: heap alignment, heap window exhaustion, page allocator roots. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::{heap, page_allocator}; + +mod results; +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel mem alloc test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + if let Err(code) = test_heap_alignment() { + utils::fail(code); + } + if let Err(code) = test_heap_exhaustion(info) { + utils::fail(code); + } + if let Err(code) = test_page_allocator_roots() { + utils::fail(code); + } + if let Err(code) = test_heap_too_large() { + utils::fail(code); + } + + utils::pass(); +} + +fn test_heap_alignment() -> Result<(), u32> { + let ptr = heap::alloc(32, 16).unwrap_or(core::ptr::null_mut()); + if ptr.is_null() { + return Err(1); + } + if (ptr as usize) & 0x0f != 0 { + return Err(2); + } + heap::dealloc(ptr, 32, 16); + Ok(()) +} + +fn test_heap_exhaustion(info: BootInfo) -> Result<(), u32> { + let window_end = info.va_base.saturating_add(info.va_len) as usize; + let available = window_end.saturating_sub(info.heap_ptr as usize); + if heap::alloc(available.saturating_add(16), 8).is_some() { + return Err(3); + } + Ok(()) +} + +fn test_page_allocator_roots() -> Result<(), u32> { + let root1 = page_allocator::alloc_root().unwrap_or(0); + let root2 = page_allocator::alloc_root().unwrap_or(0); + if root1 == 0 || root2 == 0 || root1 == root2 { + return Err(4); + } + Ok(()) +} + +fn test_heap_too_large() -> Result<(), u32> { + if heap::alloc(usize::MAX, 8).is_some() { + return Err(5); + } + Ok(()) +} diff --git a/crates/kernel/src/tests/mem_map_test.rs b/crates/kernel/src/tests/mem_map_test.rs new file mode 100644 index 0000000..3653a16 --- /dev/null +++ b/crates/kernel/src/tests/mem_map_test.rs @@ -0,0 +1,196 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Memory mapping test: user range mapping, kernel mirroring, translate/copy/peek. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::page_allocator::{self, PagePerms}; + +mod results; +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel mem map test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + let user_root = page_allocator::alloc_root().unwrap_or(0); + if user_root == 0 { + utils::fail(1); + } + + let (va_start, len) = pick_user_range(info); + + if let Err(code) = test_user_map(user_root, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_read_only_mapping(user_root, info, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_exec_mapping(user_root, info, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_kernel_sees_different_phys_before_mirror(user_root, va_start) { + utils::fail(code); + } + if let Err(code) = test_mirror(user_root, va_start, len) { + utils::fail(code); + } + if let Err(code) = test_translate(user_root, va_start) { + utils::fail(code); + } + if let Err(code) = test_copy_peek(user_root, va_start) { + utils::fail(code); + } + if let Err(code) = test_user_cannot_translate_kernel_only(user_root, info) { + utils::fail(code); + } + + log!("kernel mem map test done"); + utils::pass(); +} + +fn pick_user_range(info: BootInfo) -> (u32, usize) { + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let mut va_start = window_start.saturating_add(0x20_000); + let len = 0x1000usize; + if va_start.saturating_add(len as u32) > window_end { + va_start = window_start.saturating_add(0x1000); + } + (va_start, len) +} + +fn test_user_map(user_root: u32, va_start: u32, len: usize) -> Result<(), u32> { + // Map a user R/W range into the user root. + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, va_start, len, perms) { + return Err(2); + } + // Verify the mapping exists and can be written via the user root. + let phys = page_allocator::translate(user_root, va_start).unwrap_or(0); + if phys == 0 { + return Err(12); + } + let data = [0x0bu8, 0x0c, 0x0d, 0x0e]; + if !page_allocator::copy_user(user_root, va_start, &data) { + return Err(13); + } + Ok(()) +} + +fn test_read_only_mapping( + user_root: u32, + info: BootInfo, + base_va: u32, + len: usize, +) -> Result<(), u32> { + // Map a user read-only range and ensure writes are rejected by copy_user. + let window_end = info.va_base.saturating_add(info.va_len); + let mut ro_va = base_va.saturating_add(len as u32).saturating_add(0x1000); + if ro_va.saturating_add(len as u32) > window_end { + ro_va = info.va_base.saturating_add(0x2000); + } + let perms = PagePerms::new(true, false, false, true); + if !page_allocator::map_range_for_root(user_root, ro_va, len, perms) { + return Err(10); + } + let data = [0x5au8, 0xa5]; + if page_allocator::copy_user(user_root, ro_va, &data) { + return Err(11); + } + Ok(()) +} + +fn test_exec_mapping( + user_root: u32, + info: BootInfo, + base_va: u32, + len: usize, +) -> Result<(), u32> { + // Map a user exec-only range and ensure writes are rejected. + let window_end = info.va_base.saturating_add(info.va_len); + let mut exec_va = base_va.saturating_add(len as u32).saturating_add(0x2000); + if exec_va.saturating_add(len as u32) > window_end { + exec_va = info.va_base.saturating_add(0x3000); + } + let perms = PagePerms::new(false, false, true, true); + if !page_allocator::map_range_for_root(user_root, exec_va, len, perms) { + return Err(14); + } + let data = [0x7eu8, 0x7f]; + if page_allocator::copy_user(user_root, exec_va, &data) { + return Err(15); + } + Ok(()) +} + +fn test_mirror(user_root: u32, va_start: u32, len: usize) -> Result<(), u32> { + // Mirror the user range into the kernel root so it is accessible in kernel. + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::mirror_user_range_into_kernel(user_root, va_start, len, perms) { + return Err(3); + } + Ok(()) +} + +fn test_kernel_sees_different_phys_before_mirror(user_root: u32, va_start: u32) -> Result<(), u32> { + // Ensure kernel/user roots resolve the same VA to different physical pages pre-mirror. + let kernel_root = page_allocator::current_root(); + let phys_user = page_allocator::translate(user_root, va_start).unwrap_or(0); + let phys_kernel = page_allocator::translate(kernel_root, va_start).unwrap_or(0); + if phys_user == 0 || phys_kernel == 0 { + return Err(9); + } + if phys_user == phys_kernel { + return Err(9); + } + Ok(()) +} + +fn test_translate(user_root: u32, va_start: u32) -> Result<(), u32> { + // Ensure mirror caused kernel/user translations to resolve to the same physical page. + let kernel_root = page_allocator::current_root(); + let phys_user = page_allocator::translate(user_root, va_start).unwrap_or(0); + let phys_kernel = page_allocator::translate(kernel_root, va_start).unwrap_or(0); + if phys_user == 0 || phys_kernel == 0 || phys_user != phys_kernel { + return Err(4); + } + Ok(()) +} + +fn test_copy_peek(user_root: u32, va_start: u32) -> Result<(), u32> { + // Copy via user root and read via kernel root to verify shared physical mapping. + let kernel_root = page_allocator::current_root(); + let data = [0x11u8, 0x22, 0x33, 0x44]; + if !page_allocator::copy(user_root, va_start, &data) { + return Err(5); + } + let word = page_allocator::peek_word(kernel_root, va_start).unwrap_or(0); + if word != 0x4433_2211 { + return Err(6); + } + Ok(()) +} + +fn test_user_cannot_translate_kernel_only(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Map kernel-only memory and ensure user root cannot translate it. + let kernel_only_va = info.va_base.saturating_add(0x300_000); + let len = 0x1000usize; + if !page_allocator::map_kernel_range(kernel_only_va, len, PagePerms::kernel_rw()) { + return Err(7); + } + if page_allocator::translate(user_root, kernel_only_va).is_some() { + return Err(8); + } + Ok(()) +} diff --git a/crates/kernel/src/tests/results.rs b/crates/kernel/src/tests/results.rs index 08f6cf4..9dd4821 100644 --- a/crates/kernel/src/tests/results.rs +++ b/crates/kernel/src/tests/results.rs @@ -25,5 +25,7 @@ impl TestResults { pub unsafe fn write_results(results: TestResults) { let ptr = TEST_RESULTS_ADDR as *mut TestResults; - ptr.write_volatile(results); + unsafe { + ptr.write_volatile(results); + } } diff --git a/crates/kernel/src/tests/utils.rs b/crates/kernel/src/tests/utils.rs index 296bab0..cd8534b 100644 --- a/crates/kernel/src/tests/utils.rs +++ b/crates/kernel/src/tests/utils.rs @@ -1,11 +1,12 @@ use clibc::log; use kernel::memory::{heap, page_allocator}; use kernel::{trap, BootInfo}; +use crate::results; #[path = "../init.rs"] mod init; -pub fn init_test_kernel(boot_info_ptr: *const BootInfo) { +pub fn init_test_kernel(boot_info_ptr: *const BootInfo) -> BootInfo { let boot_info = unsafe { boot_info_ptr.as_ref() }; if let Some(info) = init::init_boot_info(boot_info) { unsafe { @@ -13,8 +14,26 @@ pub fn init_test_kernel(boot_info_ptr: *const BootInfo) { heap::init(info.heap_ptr, info.va_base, info.va_len); } trap::init_trap_vector(info.kstack_top); + let info_copy = *info; + log!("kernel initialized"); + info_copy } else { panic!("init_test_kernel: missing boot info"); } - log!("kernel initialized"); +} + +pub fn pass() -> ! { + unsafe { results::write_results(results::TestResults::pass(0)) }; + halt(); +} + +pub fn fail(code: u32) -> ! { + unsafe { results::write_results(results::TestResults::fail(code)) }; + halt(); +} + +#[inline(never)] +pub fn halt() -> ! { + unsafe { core::arch::asm!("ebreak") }; + loop {} } diff --git a/crates/types/src/mmu.rs b/crates/types/src/mmu.rs index f529b66..c186ab5 100644 --- a/crates/types/src/mmu.rs +++ b/crates/types/src/mmu.rs @@ -25,9 +25,13 @@ pub const SV32_DIRECT_MAP_BASE: u32 = 0x4000_0000; /// Simple permission descriptor for Sv32 mappings. #[derive(Clone, Copy, Debug)] pub struct Sv32PagePerms { + /// Allow load access (read data from this page). pub read: bool, + /// Allow store access (write data to this page). pub write: bool, + /// Allow instruction fetches from this page. pub exec: bool, + /// Mark the page as user-accessible (U bit set). pub user: bool, } From be683f182848f18dacee9566baeb88fb3171490c Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 15:34:40 +0200 Subject: [PATCH 64/70] Refactor program window mapping Extract program window mapping into a helper with clearer intent. Keep the first page RWX for result writes at 0x100, map remaining code pages RX, and map data/stack/heap RW. Add inline documentation explaining the mapping split and rationale. --- crates/kernel/src/task/prep.rs | 43 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/kernel/src/task/prep.rs b/crates/kernel/src/task/prep.rs index 356eb45..e8906cd 100644 --- a/crates/kernel/src/task/prep.rs +++ b/crates/kernel/src/task/prep.rs @@ -57,10 +57,7 @@ pub fn prep_program_task( root_ppn ); } - let perms = mmu::PagePerms::user_rwx(); - if !mmu::map_range_for_root(root_ppn, PROGRAM_VA_BASE, PROGRAM_WINDOW_BYTES, perms) { - panic!("launch_program: mapping failed (root=0x{:x})", root_ppn); - } + map_program_window(root_ppn, code.len()); // Copy the full program image starting at VA 0 so section offsets (e.g. .text at 0x400) // land where the ELF expected them. Entry offset is provided by the caller. @@ -156,3 +153,41 @@ pub fn prep_program_task( Some(task) } + +fn align_up(value: usize, align: usize) -> usize { + if align == 0 { + return value; + } + (value + (align - 1)) & !(align - 1) +} + +/// Map the program window so code pages are RX and data/stack/heap are RW. +/// The first page stays RWX because the program writes its result at 0x100. +fn map_program_window(root_ppn: u32, code_len: usize) { + let code_len = align_up(code_len, SV32_PAGE_SIZE); + if code_len > PROGRAM_WINDOW_BYTES { + panic!("launch_program: code window exceeds program window"); + } + let first_page_len = core::cmp::min(code_len, SV32_PAGE_SIZE); + let first_page_perms = mmu::PagePerms::user_rwx(); + // Page 0 hosts the result header at 0x100, so keep it writable. + if !mmu::map_range_for_root(root_ppn, PROGRAM_VA_BASE, first_page_len, first_page_perms) { + panic!("launch_program: first page mapping failed (root=0x{:x})", root_ppn); + } + if code_len > SV32_PAGE_SIZE { + let code_perms = mmu::PagePerms::new(true, false, true, true); + let code_start = PROGRAM_VA_BASE.wrapping_add(SV32_PAGE_SIZE as u32); + let code_rest = code_len.saturating_sub(SV32_PAGE_SIZE); + // Remaining code pages are RX-only to protect program text. + if !mmu::map_range_for_root(root_ppn, code_start, code_rest, code_perms) { + panic!("launch_program: code mapping failed (root=0x{:x})", root_ppn); + } + } + let data_start = PROGRAM_VA_BASE.wrapping_add(code_len as u32); + let data_len = PROGRAM_WINDOW_BYTES.saturating_sub(code_len); + let data_perms = mmu::PagePerms::new(true, true, false, true); + // Data/stack/heap region is RW, non-exec. + if !mmu::map_range_for_root(root_ppn, data_start, data_len, data_perms) { + panic!("launch_program: data mapping failed (root=0x{:x})", root_ppn); + } +} From 77997a1238776d5781c2eddecbc38df770f1f2a7 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 17:08:53 +0200 Subject: [PATCH 65/70] Add kernel memory edge tests and remap semantics Add new kernel test bins for map/heap/page allocator edge cases Cover multi-L2 mapping, mirror gaps, map_to_physical aliasing, and atomic copy_user Make copy_user preflight all pages and remap existing PTEs with new perms --- crates/kernel/Cargo.toml | 15 + crates/kernel/src/memory/page_allocator.rs | 20 +- crates/kernel/src/tests/heap_edge_test.rs | 89 ++++ crates/kernel/src/tests/mem_map_edge_test.rs | 441 +++++++++++++++++++ crates/kernel/src/tests/page_alloc_test.rs | 77 ++++ crates/types/src/mmu.rs | 22 +- 6 files changed, 653 insertions(+), 11 deletions(-) create mode 100644 crates/kernel/src/tests/heap_edge_test.rs create mode 100644 crates/kernel/src/tests/mem_map_edge_test.rs create mode 100644 crates/kernel/src/tests/page_alloc_test.rs diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 45c87cc..d110122 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -38,3 +38,18 @@ required-features = ["guest_kernel"] name = "kernel_mem_map_test" path = "src/tests/mem_map_test.rs" required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_mem_map_edge_test" +path = "src/tests/mem_map_edge_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_page_alloc_test" +path = "src/tests/page_alloc_test.rs" +required-features = ["guest_kernel"] + +[[bin]] +name = "kernel_heap_edge_test" +path = "src/tests/heap_edge_test.rs" +required-features = ["guest_kernel"] diff --git a/crates/kernel/src/memory/page_allocator.rs b/crates/kernel/src/memory/page_allocator.rs index 8171a90..80765bc 100644 --- a/crates/kernel/src/memory/page_allocator.rs +++ b/crates/kernel/src/memory/page_allocator.rs @@ -314,13 +314,12 @@ pub fn copy(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { true } -/// Copy data into a user VA range, failing if any page is not user-writable. +/// Copy data into a user VA range atomically, failing if any page is not user-writable. pub fn copy_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { if data.is_empty() { return true; } let mut remaining = data.len(); - let mut src_off = 0usize; let mut va = va_start; while remaining > 0 { let pte = match leaf_pte(root_ppn, va) { @@ -332,6 +331,23 @@ pub fn copy_user(root_ppn: u32, va_start: u32, data: &[u8]) -> bool { if !is_user || !can_write { return false; } + let phys = match translate(root_ppn, va) { + Some(p) => p, + None => return false, + }; + if direct_map_addr(phys).is_none() { + return false; + } + let page_off = (va as usize) & (PAGE_SIZE - 1); + let to_copy = cmp::min(remaining, PAGE_SIZE - page_off); + remaining -= to_copy; + va = va.wrapping_add(to_copy as u32); + } + + let mut remaining = data.len(); + let mut src_off = 0usize; + let mut va = va_start; + while remaining > 0 { let phys = match translate(root_ppn, va) { Some(p) => p, None => return false, diff --git a/crates/kernel/src/tests/heap_edge_test.rs b/crates/kernel/src/tests/heap_edge_test.rs new file mode 100644 index 0000000..823b5b1 --- /dev/null +++ b/crates/kernel/src/tests/heap_edge_test.rs @@ -0,0 +1,89 @@ +#![no_std] +#![no_main] + +// Heap edge tests: invalid layouts and monotonic bump behavior. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::heap; + +mod results; +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel heap edge test boot"); + let _info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + if let Err(code) = test_invalid_layouts() { + utils::fail(code); + } + if let Err(code) = test_monotonic_bump_and_data() { + utils::fail(code); + } + + log!("kernel heap edge test done"); + utils::pass(); +} + +fn test_invalid_layouts() -> Result<(), u32> { + // Description: heap::alloc must reject invalid size/align values. + log!("test: invalid heap layouts are rejected"); + log!("subtest: zero size and zero align are rejected"); + + if heap::alloc(0, 8).is_some() { + return Err(10); + } + if heap::alloc(16, 0).is_some() { + return Err(11); + } + + log!("subtest: non power-of-two alignment is rejected"); + if heap::alloc(16, 3).is_some() { + return Err(12); + } + + log!("subtest: size overflow is rejected"); + if heap::alloc(usize::MAX, 16).is_some() { + return Err(13); + } + Ok(()) +} + +fn test_monotonic_bump_and_data() -> Result<(), u32> { + // Description: allocations should be monotonic and memory is usable. + log!("test: monotonic bump allocator behavior"); + log!("subtest: allocations are ordered and writable"); + + let a = heap::alloc(32, 8).unwrap_or(core::ptr::null_mut()); + let b = heap::alloc(32, 8).unwrap_or(core::ptr::null_mut()); + if a.is_null() || b.is_null() { + return Err(20); + } + if (b as usize) <= (a as usize) { + return Err(21); + } + unsafe { + a.write_bytes(0xab, 32); + b.write_bytes(0xcd, 32); + } + let a_first = unsafe { a.read() }; + let b_first = unsafe { b.read() }; + if a_first != 0xab || b_first != 0xcd { + return Err(22); + } + + log!("subtest: dealloc is a no-op and allocations keep increasing"); + heap::dealloc(a, 32, 8); + let c = heap::alloc(16, 8).unwrap_or(core::ptr::null_mut()); + if c.is_null() || (c as usize) <= (b as usize) { + return Err(23); + } + Ok(()) +} diff --git a/crates/kernel/src/tests/mem_map_edge_test.rs b/crates/kernel/src/tests/mem_map_edge_test.rs new file mode 100644 index 0000000..dedcb51 --- /dev/null +++ b/crates/kernel/src/tests/mem_map_edge_test.rs @@ -0,0 +1,441 @@ +#![no_std] +#![no_main] + +extern crate alloc; + +// Memory mapping edge-case tests: alignment, boundary spanning, map_to_physical, mirror gaps, +// copy_user atomicity, and remap overrides. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::page_allocator::{self, PagePerms}; + +mod results; +mod utils; + +const PAGE_SIZE: usize = 0x1000; +const L1_SPAN: u32 = 1 << 22; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel mem map edge test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + let user_root = page_allocator::alloc_root().unwrap_or(0); + if user_root == 0 { + utils::fail(1); + } + + if let Err(code) = test_unaligned_map_and_translate(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_cross_l1_boundary(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_multiple_l2_tables(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_zero_len_map_no_effect(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_map_to_physical_alignment_and_alias(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_mirror_gap_behavior(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_copy_user_atomic(user_root, info) { + utils::fail(code); + } + if let Err(code) = test_remap_override_perms(user_root, info) { + utils::fail(code); + } + + log!("kernel mem map edge test done"); + utils::pass(); +} + +fn test_unaligned_map_and_translate(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: map an unaligned range that crosses a page boundary and verify translations + // and data access on the mapped pages. + log!("test: unaligned map + translate"); + log!("subtest: map unaligned range and confirm translations are present"); + + let base = pick_user_va(info, 0x4000); + let va_start = base.wrapping_add(37); + let len = PAGE_SIZE + 123; + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, va_start, len, perms) { + return Err(10); + } + + let first_phys = page_allocator::translate(user_root, va_start).unwrap_or(0); + let last_phys = page_allocator::translate(user_root, va_start.wrapping_add(len as u32 - 1)) + .unwrap_or(0); + if first_phys == 0 || last_phys == 0 { + return Err(11); + } + + log!("subtest: write data and confirm it can be read back"); + let data = [0x12u8, 0x34, 0x56, 0x78]; + if !page_allocator::copy(user_root, va_start, &data) { + return Err(12); + } + let word = page_allocator::peek_word(user_root, va_start).unwrap_or(0); + if word != 0x7856_3412 { + return Err(13); + } + Ok(()) +} + +fn test_cross_l1_boundary(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: map a range that crosses a VPN1 boundary and ensure both pages are mapped + // and writable. + log!("test: cross L1 boundary mapping"); + log!("subtest: pick a range that crosses a 4 MiB boundary"); + + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let next_boundary = align_up_u32(window_start.saturating_add(0x1000), L1_SPAN); + let start = next_boundary.saturating_sub(PAGE_SIZE as u32); + let end = start.saturating_add((PAGE_SIZE * 2) as u32); + if start < window_start || end > window_end { + log!("subtest: skipped (window too small for boundary test)"); + return Ok(()); + } + + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, start, PAGE_SIZE * 2, perms) { + return Err(20); + } + + log!("subtest: confirm translations and data access on both pages"); + let first_phys = page_allocator::translate(user_root, start).unwrap_or(0); + let second_phys = page_allocator::translate(user_root, start.wrapping_add(PAGE_SIZE as u32)) + .unwrap_or(0); + if first_phys == 0 || second_phys == 0 { + return Err(21); + } + + let first_data = [0xa1u8, 0xa2, 0xa3, 0xa4]; + let second_data = [0xb1u8, 0xb2, 0xb3, 0xb4]; + if !page_allocator::copy_user(user_root, start, &first_data) { + return Err(22); + } + if !page_allocator::copy_user(user_root, start.wrapping_add(PAGE_SIZE as u32), &second_data) { + return Err(23); + } + let first_word = page_allocator::peek_word(user_root, start).unwrap_or(0); + let second_word = page_allocator::peek_word(user_root, start.wrapping_add(PAGE_SIZE as u32)) + .unwrap_or(0); + if first_word != 0xa4a3_a2a1 || second_word != 0xb4b3_b2b1 { + return Err(24); + } + Ok(()) +} + +fn test_zero_len_map_no_effect(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: mapping a zero-length range should be a no-op. + log!("test: zero-length map is a no-op"); + log!("subtest: ensure translation stays absent"); + + let va = pick_user_va(info, 0x9000); + if page_allocator::translate(user_root, va).is_some() { + return Err(30); + } + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, va, 0, perms) { + return Err(31); + } + if page_allocator::translate(user_root, va).is_some() { + return Err(32); + } + Ok(()) +} + +fn test_multiple_l2_tables(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: map pages in multiple VPN1 regions to ensure more than two L2 tables + // can be allocated and accessed. + log!("test: multiple L2 tables via sparse VPN1 mapping"); + log!("subtest: map one page in three distinct VPN1 regions"); + + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let first_region = align_up_u32(window_start.saturating_add(0x1000), L1_SPAN); + let third_region = first_region.saturating_add(L1_SPAN * 2); + if third_region.saturating_add(PAGE_SIZE as u32) > window_end { + log!("subtest: skipped (window too small for multi-L2 test)"); + return Ok(()); + } + + let perms = PagePerms::new(true, true, false, true); + let mut vas = [0u32; 3]; + for (idx, va) in vas.iter_mut().enumerate() { + let region = first_region.saturating_add(L1_SPAN * idx as u32); + *va = align_down_u32(region, PAGE_SIZE as u32); + if !page_allocator::map_range_for_root(user_root, *va, PAGE_SIZE, perms) { + return Err(34); + } + } + + log!("subtest: write data and verify translations across each region"); + for (idx, va) in vas.iter().enumerate() { + let phys = page_allocator::translate(user_root, *va).unwrap_or(0); + if phys == 0 { + return Err(35); + } + let data = [0x90u8 + idx as u8, 0x91 + idx as u8, 0x92 + idx as u8, 0x93 + idx as u8]; + if !page_allocator::copy_user(user_root, *va, &data) { + return Err(36); + } + let word = page_allocator::peek_word(user_root, *va).unwrap_or(0); + let expected = (0x93u32 + idx as u32) << 24 + | (0x92u32 + idx as u32) << 16 + | (0x91u32 + idx as u32) << 8 + | (0x90u32 + idx as u32); + if word != expected { + return Err(37); + } + } + Ok(()) +} + +fn test_map_to_physical_alignment_and_alias( + user_root: u32, + info: BootInfo, +) -> Result<(), u32> { + // Description: map_to_physical must reject unaligned physical addresses and alias + // when aligned. + log!("test: map_to_physical alignment + aliasing"); + log!("subtest: create a source mapping to obtain a physical page"); + + let source_va = align_down_u32(pick_user_va(info, 0x12000), PAGE_SIZE as u32); + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, source_va, PAGE_SIZE, perms) { + return Err(40); + } + let source_phys = page_allocator::translate(user_root, source_va).unwrap_or(0); + if source_phys == 0 || source_phys % PAGE_SIZE != 0 { + return Err(41); + } + + log!("subtest: unaligned map_to_physical is rejected"); + let target_va = align_down_u32(pick_user_va(info, 0x18000), PAGE_SIZE as u32); + if page_allocator::map_physical_range_for_root( + user_root, + target_va, + (source_phys as u32).wrapping_add(1), + PAGE_SIZE, + perms, + ) { + return Err(42); + } + if page_allocator::translate(user_root, target_va).is_some() { + return Err(43); + } + + log!("subtest: aligned map_to_physical creates an alias"); + if !page_allocator::map_physical_range_for_root( + user_root, + target_va, + source_phys as u32, + PAGE_SIZE, + perms, + ) { + return Err(44); + } + let aliased = page_allocator::translate(user_root, target_va).unwrap_or(0); + if aliased != source_phys { + return Err(45); + } + + let data = [0x0cu8, 0x0d, 0x0e, 0x0f]; + if !page_allocator::copy_user(user_root, target_va, &data) { + return Err(46); + } + let word = page_allocator::peek_word(user_root, source_va).unwrap_or(0); + if word != 0x0f0e_0d0c { + return Err(47); + } + Ok(()) +} + +fn test_mirror_gap_behavior(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: mirroring a range with an unmapped page should fail and only mirror + // pages visited before the gap. + log!("test: mirror range with a gap"); + log!("subtest: create two mapped pages with an unmapped gap"); + + let base = align_down_u32(pick_user_va(info, 0x1e000), PAGE_SIZE as u32); + let perms = PagePerms::new(true, true, false, true); + if !page_allocator::map_range_for_root(user_root, base, PAGE_SIZE, perms) { + return Err(50); + } + if !page_allocator::map_range_for_root( + user_root, + base.wrapping_add((PAGE_SIZE * 2) as u32), + PAGE_SIZE, + perms, + ) { + return Err(51); + } + + log!("subtest: mirror across the gap and verify partial mirroring"); + let kernel_root = page_allocator::current_root(); + let gap_va = base.wrapping_add(PAGE_SIZE as u32); + let end_va = base.wrapping_add((PAGE_SIZE * 2) as u32); + let kernel_before_gap = page_allocator::translate(kernel_root, gap_va); + let kernel_before_end = page_allocator::translate(kernel_root, end_va); + let mirror_ok = page_allocator::mirror_user_range_into_kernel( + user_root, + base, + PAGE_SIZE * 3, + perms, + ); + if mirror_ok { + return Err(52); + } + let user_phys = page_allocator::translate(user_root, base).unwrap_or(0); + let kernel_phys = page_allocator::translate(kernel_root, base).unwrap_or(0); + if user_phys == 0 || kernel_phys != user_phys { + return Err(53); + } + if page_allocator::translate(user_root, gap_va).is_some() { + return Err(54); + } + let kernel_after_gap = page_allocator::translate(kernel_root, gap_va); + if kernel_after_gap != kernel_before_gap { + return Err(55); + } + let kernel_after_end = page_allocator::translate(kernel_root, end_va); + if kernel_after_end != kernel_before_end { + return Err(56); + } + Ok(()) +} + +fn test_copy_user_atomic(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: copy_user should be atomic (all-or-nothing) across page boundaries. + log!("test: copy_user atomicity"); + log!("subtest: create writable + read-only pages"); + + let base = align_down_u32(pick_user_va(info, 0x26000), PAGE_SIZE as u32); + let perms_rw = PagePerms::new(true, true, false, true); + let perms_ro = PagePerms::new(true, false, false, true); + if !page_allocator::map_range_for_root(user_root, base, PAGE_SIZE, perms_rw) { + return Err(60); + } + if !page_allocator::map_range_for_root( + user_root, + base.wrapping_add(PAGE_SIZE as u32), + PAGE_SIZE, + perms_ro, + ) { + return Err(61); + } + + log!("subtest: seed data on both pages and near the boundary"); + let seed_first = [0x11u8, 0x22, 0x33, 0x44]; + let seed_second = [0xaau8, 0xbb, 0xcc, 0xdd]; + if !page_allocator::copy(user_root, base, &seed_first) { + return Err(62); + } + if !page_allocator::copy( + user_root, + base.wrapping_add(PAGE_SIZE as u32), + &seed_second, + ) { + return Err(63); + } + let boundary_seed = [0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + let boundary_start = base.wrapping_add(PAGE_SIZE as u32 - 8); + if !page_allocator::copy(user_root, boundary_start, &boundary_seed) { + return Err(64); + } + + log!("subtest: attempt a cross-page write and confirm no bytes changed"); + let mut big = [0u8; 16]; + for (i, byte) in big.iter_mut().enumerate() { + *byte = (0x80 + i as u8) as u8; + } + let cross_start = boundary_start; + if page_allocator::copy_user(user_root, cross_start, &big) { + return Err(65); + } + let first_word = page_allocator::peek_word(user_root, base).unwrap_or(0); + let boundary_word0 = page_allocator::peek_word(user_root, boundary_start).unwrap_or(0); + let boundary_word1 = page_allocator::peek_word( + user_root, + boundary_start.wrapping_add(4), + ) + .unwrap_or(0); + let second_word = page_allocator::peek_word( + user_root, + base.wrapping_add(PAGE_SIZE as u32), + ) + .unwrap_or(0); + if first_word != 0x4433_2211 + || boundary_word0 != 0x0403_0201 + || boundary_word1 != 0x0807_0605 + || second_word != 0xddcc_bbaa + { + return Err(66); + } + Ok(()) +} + +fn test_remap_override_perms(user_root: u32, info: BootInfo) -> Result<(), u32> { + // Description: remapping an existing VA should override permissions. + log!("test: remap overrides permissions"); + log!("subtest: map RW, write data, then remap RO"); + + let va = align_down_u32(pick_user_va(info, 0x2e000), PAGE_SIZE as u32); + let perms_rw = PagePerms::new(true, true, false, true); + let perms_ro = PagePerms::new(true, false, false, true); + if !page_allocator::map_range_for_root(user_root, va, PAGE_SIZE, perms_rw) { + return Err(70); + } + let first = [0x0au8, 0x0b, 0x0c, 0x0d]; + if !page_allocator::copy_user(user_root, va, &first) { + return Err(71); + } + if !page_allocator::map_range_for_root(user_root, va, PAGE_SIZE, perms_ro) { + return Err(72); + } + + log!("subtest: verify write is rejected and existing data remains"); + let second = [0xf1u8, 0xf2, 0xf3, 0xf4]; + if page_allocator::copy_user(user_root, va, &second) { + return Err(73); + } + let word = page_allocator::peek_word(user_root, va).unwrap_or(0); + if word != 0x0d0c_0b0a { + return Err(74); + } + Ok(()) +} + +fn pick_user_va(info: BootInfo, offset: u32) -> u32 { + let window_start = info.va_base; + let window_end = info.va_base.saturating_add(info.va_len); + let mut candidate = window_start.saturating_add(offset); + let len = PAGE_SIZE as u32 * 4; + if candidate.saturating_add(len) > window_end { + candidate = window_start.saturating_add(PAGE_SIZE as u32); + } + candidate +} + +const fn align_down_u32(val: u32, align: u32) -> u32 { + val & !(align - 1) +} + +const fn align_up_u32(val: u32, align: u32) -> u32 { + (val + (align - 1)) & !(align - 1) +} diff --git a/crates/kernel/src/tests/page_alloc_test.rs b/crates/kernel/src/tests/page_alloc_test.rs new file mode 100644 index 0000000..2d1c440 --- /dev/null +++ b/crates/kernel/src/tests/page_alloc_test.rs @@ -0,0 +1,77 @@ +#![no_std] +#![no_main] + +// Page allocator tests: root zeroing and bump behavior. +use clibc::log; +use kernel::BootInfo; +use kernel::memory::page_allocator; + +mod results; +mod utils; + +#[unsafe(no_mangle)] +pub extern "C" fn _start( + input_ptr: *const u8, + input_len: usize, + boot_info_ptr: *const BootInfo, +) { + log!("kernel page allocator test boot"); + let info = utils::init_test_kernel(boot_info_ptr); + + clibc::logf!("kernel test input len: %d", input_len as u32); + let _input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + + if let Err(code) = test_alloc_root_zeroed(info) { + utils::fail(code); + } + if let Err(code) = test_bump_allocator_behavior() { + utils::fail(code); + } + + log!("kernel page allocator test done"); + utils::pass(); +} + +fn test_alloc_root_zeroed(info: BootInfo) -> Result<(), u32> { + // Description: freshly allocated roots should be zeroed (no valid mappings). + log!("test: alloc_root yields zeroed page table"); + log!("subtest: ensure translation is absent for a fresh root"); + + let root = page_allocator::alloc_root().unwrap_or(0); + if root == 0 { + return Err(10); + } + let va = info.va_base.saturating_add(0x1000); + if page_allocator::translate(root, va).is_some() { + return Err(11); + } + Ok(()) +} + +fn test_bump_allocator_behavior() -> Result<(), u32> { + // Description: bumping should skip page frames and allow exhaustion testing. + log!("test: bump_page_allocator skips frames"); + log!("subtest: bump to a higher ppn and verify allocations skip ahead"); + + let first = page_allocator::alloc_root().unwrap_or(0); + if first == 0 { + return Err(20); + } + let bump_to = first.saturating_add(4); + page_allocator::bump_page_allocator(bump_to); + let second = page_allocator::alloc_root().unwrap_or(0); + if second < bump_to { + return Err(21); + } + + log!("subtest: bump to limit and verify allocator is exhausted"); + let limit = page_allocator::total_ppn().unwrap_or(0); + if limit == 0 { + return Err(22); + } + page_allocator::bump_page_allocator(limit); + if page_allocator::alloc_root().is_some() { + return Err(23); + } + Ok(()) +} diff --git a/crates/types/src/mmu.rs b/crates/types/src/mmu.rs index c186ab5..0562b4c 100644 --- a/crates/types/src/mmu.rs +++ b/crates/types/src/mmu.rs @@ -217,21 +217,25 @@ fn map_page( }; let l2_entry_addr = l2_base + vpn0 as usize * mem::size_of::(); - if let Some(existing) = pt.read_pte(l2_entry_addr) { - if existing & SV32_PTE_V != 0 { - // Already mapped. - return true; - } - } + let existing = pt.read_pte(l2_entry_addr).unwrap_or(0); + let existing_valid = (existing & SV32_PTE_V) != 0; + let existing_ppn = existing >> 10; - let leaf_ppn = match phys_override { - Some(phys) => { + let leaf_ppn = match (existing_valid, phys_override) { + (true, Some(phys)) => { + if (phys as usize) % page_size != 0 { + return false; + } + phys / page_size as u32 + } + (true, None) => existing_ppn, + (false, Some(phys)) => { if (phys as usize) % page_size != 0 { return false; } phys / page_size as u32 } - None => match pt.alloc_frame() { + (false, None) => match pt.alloc_frame() { Some(ppn) => { pt.zero_frame(ppn); ppn From a508354cba3b636bd5d48380d2af6a763fe705d9 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 17:12:31 +0200 Subject: [PATCH 66/70] moved kernel mem tests --- Makefile | 2 +- crates/kernel/Cargo.toml | 12 ++++++------ crates/kernel/src/{ => memory}/tests/first_test.rs | 2 ++ .../kernel/src/{ => memory}/tests/heap_edge_test.rs | 2 ++ .../kernel/src/{ => memory}/tests/mem_alloc_test.rs | 2 ++ .../src/{ => memory}/tests/mem_map_edge_test.rs | 2 ++ crates/kernel/src/{ => memory}/tests/mem_map_test.rs | 2 ++ .../kernel/src/{ => memory}/tests/page_alloc_test.rs | 2 ++ 8 files changed, 19 insertions(+), 7 deletions(-) rename crates/kernel/src/{ => memory}/tests/first_test.rs (89%) rename crates/kernel/src/{ => memory}/tests/heap_edge_test.rs (97%) rename crates/kernel/src/{ => memory}/tests/mem_alloc_test.rs (96%) rename crates/kernel/src/{ => memory}/tests/mem_map_edge_test.rs (99%) rename crates/kernel/src/{ => memory}/tests/mem_map_test.rs (98%) rename crates/kernel/src/{ => memory}/tests/page_alloc_test.rs (97%) diff --git a/Makefile b/Makefile index 4962f1d..6532543 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ kernel: @mkdir -p $(KERNEL_OUT_DIR) @$(AVM32) all --bin kernel --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/main.rs @echo "=== Building kernel test ELFs ===" - @$(foreach bin,$(KERNEL_TEST_BINS),$(AVM32) all --bin $(bin) --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/tests/$(patsubst kernel_%,%,$(bin)).rs;) + @$(foreach bin,$(KERNEL_TEST_BINS),$(AVM32) all --bin $(bin) --manifest-path $(KERNEL_MANIFEST) --features guest_kernel --out-dir $(KERNEL_OUT_DIR) --src crates/kernel/src/memory/tests/$(patsubst kernel_%,%,$(bin)).rs;) run_examples: @echo "=== Building example programs ===" diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index d110122..d280ff1 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -26,30 +26,30 @@ required-features = ["guest_kernel"] [[bin]] name = "kernel_first_test" -path = "src/tests/first_test.rs" +path = "src/memory/tests/first_test.rs" required-features = ["guest_kernel"] [[bin]] name = "kernel_mem_alloc_test" -path = "src/tests/mem_alloc_test.rs" +path = "src/memory/tests/mem_alloc_test.rs" required-features = ["guest_kernel"] [[bin]] name = "kernel_mem_map_test" -path = "src/tests/mem_map_test.rs" +path = "src/memory/tests/mem_map_test.rs" required-features = ["guest_kernel"] [[bin]] name = "kernel_mem_map_edge_test" -path = "src/tests/mem_map_edge_test.rs" +path = "src/memory/tests/mem_map_edge_test.rs" required-features = ["guest_kernel"] [[bin]] name = "kernel_page_alloc_test" -path = "src/tests/page_alloc_test.rs" +path = "src/memory/tests/page_alloc_test.rs" required-features = ["guest_kernel"] [[bin]] name = "kernel_heap_edge_test" -path = "src/tests/heap_edge_test.rs" +path = "src/memory/tests/heap_edge_test.rs" required-features = ["guest_kernel"] diff --git a/crates/kernel/src/tests/first_test.rs b/crates/kernel/src/memory/tests/first_test.rs similarity index 89% rename from crates/kernel/src/tests/first_test.rs rename to crates/kernel/src/memory/tests/first_test.rs index 9dd9c7b..6ae5036 100644 --- a/crates/kernel/src/tests/first_test.rs +++ b/crates/kernel/src/memory/tests/first_test.rs @@ -8,7 +8,9 @@ use core::slice; use clibc::log; use kernel::BootInfo; +#[path = "../../tests/utils.rs"] mod utils; +#[path = "../../tests/results.rs"] mod results; #[unsafe(no_mangle)] diff --git a/crates/kernel/src/tests/heap_edge_test.rs b/crates/kernel/src/memory/tests/heap_edge_test.rs similarity index 97% rename from crates/kernel/src/tests/heap_edge_test.rs rename to crates/kernel/src/memory/tests/heap_edge_test.rs index 823b5b1..7c7c529 100644 --- a/crates/kernel/src/tests/heap_edge_test.rs +++ b/crates/kernel/src/memory/tests/heap_edge_test.rs @@ -6,7 +6,9 @@ use clibc::log; use kernel::BootInfo; use kernel::memory::heap; +#[path = "../../tests/results.rs"] mod results; +#[path = "../../tests/utils.rs"] mod utils; #[unsafe(no_mangle)] diff --git a/crates/kernel/src/tests/mem_alloc_test.rs b/crates/kernel/src/memory/tests/mem_alloc_test.rs similarity index 96% rename from crates/kernel/src/tests/mem_alloc_test.rs rename to crates/kernel/src/memory/tests/mem_alloc_test.rs index 652f921..eb6b8c7 100644 --- a/crates/kernel/src/tests/mem_alloc_test.rs +++ b/crates/kernel/src/memory/tests/mem_alloc_test.rs @@ -8,7 +8,9 @@ use clibc::log; use kernel::BootInfo; use kernel::memory::{heap, page_allocator}; +#[path = "../../tests/results.rs"] mod results; +#[path = "../../tests/utils.rs"] mod utils; #[unsafe(no_mangle)] diff --git a/crates/kernel/src/tests/mem_map_edge_test.rs b/crates/kernel/src/memory/tests/mem_map_edge_test.rs similarity index 99% rename from crates/kernel/src/tests/mem_map_edge_test.rs rename to crates/kernel/src/memory/tests/mem_map_edge_test.rs index dedcb51..e2c3486 100644 --- a/crates/kernel/src/tests/mem_map_edge_test.rs +++ b/crates/kernel/src/memory/tests/mem_map_edge_test.rs @@ -9,7 +9,9 @@ use clibc::log; use kernel::BootInfo; use kernel::memory::page_allocator::{self, PagePerms}; +#[path = "../../tests/results.rs"] mod results; +#[path = "../../tests/utils.rs"] mod utils; const PAGE_SIZE: usize = 0x1000; diff --git a/crates/kernel/src/tests/mem_map_test.rs b/crates/kernel/src/memory/tests/mem_map_test.rs similarity index 98% rename from crates/kernel/src/tests/mem_map_test.rs rename to crates/kernel/src/memory/tests/mem_map_test.rs index 3653a16..45e5bf1 100644 --- a/crates/kernel/src/tests/mem_map_test.rs +++ b/crates/kernel/src/memory/tests/mem_map_test.rs @@ -8,7 +8,9 @@ use clibc::log; use kernel::BootInfo; use kernel::memory::page_allocator::{self, PagePerms}; +#[path = "../../tests/results.rs"] mod results; +#[path = "../../tests/utils.rs"] mod utils; #[unsafe(no_mangle)] diff --git a/crates/kernel/src/tests/page_alloc_test.rs b/crates/kernel/src/memory/tests/page_alloc_test.rs similarity index 97% rename from crates/kernel/src/tests/page_alloc_test.rs rename to crates/kernel/src/memory/tests/page_alloc_test.rs index 2d1c440..de7a80f 100644 --- a/crates/kernel/src/tests/page_alloc_test.rs +++ b/crates/kernel/src/memory/tests/page_alloc_test.rs @@ -6,7 +6,9 @@ use clibc::log; use kernel::BootInfo; use kernel::memory::page_allocator; +#[path = "../../tests/results.rs"] mod results; +#[path = "../../tests/utils.rs"] mod utils; #[unsafe(no_mangle)] From c1557b59f0728d3e5a3097d70a1e99e0624a4c53 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 18:08:09 +0200 Subject: [PATCH 67/70] Implement trap modes and split sret/mret Add machine/supervisor trap routing in CPU and use mode-aware vectors. Split sret from mret decoding and execution paths. Update ebreak handling to trap only in user mode and halt otherwise. Adjust spec runner to use Sv32 memory, map sections, and report tohost/ecall exits. --- crates/vm/src/cpu.rs | 96 +++++++-- crates/vm/src/decoder.rs | 3 +- crates/vm/src/exe.rs | 31 ++- crates/vm/src/instruction.rs | 4 + crates/vm/tests/spec_runner.rs | 349 +++++++++++++++++++++++++++++++++ 5 files changed, 457 insertions(+), 26 deletions(-) create mode 100644 crates/vm/tests/spec_runner.rs diff --git a/crates/vm/src/cpu.rs b/crates/vm/src/cpu.rs index b67e6c5..e0efe5f 100644 --- a/crates/vm/src/cpu.rs +++ b/crates/vm/src/cpu.rs @@ -15,8 +15,13 @@ pub const CSR_STVEC: u16 = 0x105; pub const CSR_SEPC: u16 = 0x141; pub const CSR_SCAUSE: u16 = 0x142; pub const CSR_STVAL: u16 = 0x143; +pub const CSR_MEPC: u16 = 0x341; +pub const CSR_MTVEC: u16 = 0x305; +pub const CSR_MCAUSE: u16 = 0x342; +pub const CSR_MTVAL: u16 = 0x343; const SCAUSE_ECALL_FROM_U: u32 = 8; const SCAUSE_ECALL_FROM_S: u32 = 9; +const SCAUSE_ECALL_FROM_M: u32 = 11; const SCAUSE_BREAKPOINT: u32 = 3; const SSTATUS_SPP: u32 = 1 << 8; @@ -24,6 +29,13 @@ const SSTATUS_SPP: u32 = 1 << 8; pub enum PrivilegeMode { User, Supervisor, + Machine, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TrapMode { + Supervisor, + Machine, } /// Represents the Central Processing Unit (CPU) of our RISC-V virtual machine. @@ -206,6 +218,7 @@ impl CPU { match prev { PrivilegeMode::User => sstatus &= !SSTATUS_SPP, PrivilegeMode::Supervisor => sstatus |= SSTATUS_SPP, + PrivilegeMode::Machine => sstatus |= SSTATUS_SPP, } let _ = self.write_csr(CSR_SSTATUS, sstatus); } @@ -226,30 +239,77 @@ impl CPU { match self.priv_mode { PrivilegeMode::User => SCAUSE_ECALL_FROM_U, PrivilegeMode::Supervisor => SCAUSE_ECALL_FROM_S, + PrivilegeMode::Machine => SCAUSE_ECALL_FROM_M, } } - fn trap_to_vector(&mut self, cause: u32, trap_value: u32, syscall_id: Option) -> bool { - if !self.write_csr(CSR_SEPC, self.pc) { - panic!("trap_to_vector: failed to write sepc"); - } - if !self.write_csr(CSR_SCAUSE, cause) { - panic!("trap_to_vector: failed to write scause"); - } - if !self.write_csr(CSR_STVAL, trap_value) { - panic!("trap_to_vector: failed to write stval"); + fn trap_to_vector( + &mut self, + mode: TrapMode, + cause: u32, + trap_value: u32, + _syscall_id: Option, + ) -> bool { + match mode { + TrapMode::Machine => { + if !self.write_csr(CSR_MEPC, self.pc) { + panic!("trap_to_vector: failed to write mepc"); + } + if !self.write_csr(CSR_MCAUSE, cause) { + panic!("trap_to_vector: failed to write mcause"); + } + if !self.write_csr(CSR_MTVAL, trap_value) { + panic!("trap_to_vector: failed to write mtval"); + } + let mtvec = match self.read_csr(CSR_MTVEC) { + Some(val) => val & !0x3, + None => return false, + }; + self.priv_mode = PrivilegeMode::Machine; + self.set_pc(mtvec) + } + TrapMode::Supervisor => { + if !self.write_csr(CSR_SEPC, self.pc) { + panic!("trap_to_vector: failed to write sepc"); + } + if !self.write_csr(CSR_SCAUSE, cause) { + panic!("trap_to_vector: failed to write scause"); + } + if !self.write_csr(CSR_STVAL, trap_value) { + panic!("trap_to_vector: failed to write stval"); + } + let stvec = match self.read_csr(CSR_STVEC) { + Some(val) => val & !0x3, + None => return false, + }; + self.set_sstatus_spp(self.priv_mode); + self.priv_mode = PrivilegeMode::Supervisor; + self.set_pc(stvec) + } } - let stvec = match self.read_csr(CSR_STVEC) { - Some(val) => val & !0x3, - None => return false, - }; - self.set_sstatus_spp(self.priv_mode); - self.priv_mode = PrivilegeMode::Supervisor; - self.set_pc(stvec) } - fn has_trap_vector(&self) -> bool { - self.csrs.contains_key(&CSR_STVEC) + fn has_trap_vector(&self) -> Option { + match self.priv_mode { + PrivilegeMode::Machine => { + if self.csrs.contains_key(&CSR_MTVEC) { + Some(TrapMode::Machine) + } else if self.csrs.contains_key(&CSR_STVEC) { + Some(TrapMode::Supervisor) + } else { + None + } + } + PrivilegeMode::Supervisor | PrivilegeMode::User => { + if self.csrs.contains_key(&CSR_STVEC) { + Some(TrapMode::Supervisor) + } else if self.csrs.contains_key(&CSR_MTVEC) { + Some(TrapMode::Machine) + } else { + None + } + } + } } /// Executes a single instruction cycle (fetch, decode, execute). diff --git a/crates/vm/src/decoder.rs b/crates/vm/src/decoder.rs index 31f7aa5..6d5ecd6 100644 --- a/crates/vm/src/decoder.rs +++ b/crates/vm/src/decoder.rs @@ -411,7 +411,8 @@ pub fn decode_full(word: u32) -> Option { match funct12 { 0 => Some(Instruction::Ecall), 1 => Some(Instruction::Ebreak), - 0x302 | 0x102 => Some(Instruction::Mret), + 0x302 => Some(Instruction::Mret), + 0x102 => Some(Instruction::Sret), _ => None, } } diff --git a/crates/vm/src/exe.rs b/crates/vm/src/exe.rs index 11580de..2b326d5 100644 --- a/crates/vm/src/exe.rs +++ b/crates/vm/src/exe.rs @@ -1,4 +1,4 @@ -use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_SATP, CSR_SEPC, SCAUSE_BREAKPOINT}; +use super::{Instruction, MemoryAccessKind, Memory, CPU, CSR_MEPC, CSR_SATP, CSR_SEPC, SCAUSE_BREAKPOINT}; use crate::console::{console_write, CONSOLE_WRITE_ID}; use crate::memory::VirtualAddress; use crate::instruction::CsrOp; @@ -804,8 +804,8 @@ impl CPU { } return true; } - if self.has_trap_vector() { - if !self.trap_to_vector(self.ecall_cause(), 0, Some(call_id)) { + if let Some(trap_mode) = self.has_trap_vector() { + if !self.trap_to_vector(trap_mode, self.ecall_cause(), 0, Some(call_id)) { panic!( "trap_to_vector returned false for ecall id={} pc=0x{:08x}", call_id, self.pc @@ -876,15 +876,32 @@ impl CPU { Instruction::Ebreak => { // EDUCATIONAL: EBREAK - Environment Break - for debugging // In real systems, this would trigger a debugger breakpoint - if self.priv_mode == super::PrivilegeMode::User && self.has_trap_vector() { - if !self.trap_to_vector(SCAUSE_BREAKPOINT, 0, None) { - panic!("trap_to_vector returned false for ebreak pc=0x{:08x}", self.pc); + if self.priv_mode == super::PrivilegeMode::User { + if let Some(trap_mode) = self.has_trap_vector() { + if !self.trap_to_vector(trap_mode, SCAUSE_BREAKPOINT, 0, None) { + panic!( + "trap_to_vector returned false for ebreak pc=0x{:08x}", + self.pc + ); + } + return true; } - return true; } return false; } Instruction::Mret => { + let target = match self.read_csr(CSR_MEPC).or_else(|| self.read_csr(CSR_SEPC)) { + Some(v) => v, + None => return false, + }; + let prev = self.take_sstatus_spp(); + if !self.set_pc(target) { + return false; + } + self.priv_mode = prev; + return true; + } + Instruction::Sret => { let target = match self.read_csr(CSR_SEPC) { Some(v) => v, None => return false, diff --git a/crates/vm/src/instruction.rs b/crates/vm/src/instruction.rs index e144d2e..fc28c0a 100644 --- a/crates/vm/src/instruction.rs +++ b/crates/vm/src/instruction.rs @@ -446,6 +446,8 @@ pub enum Instruction { /// MRET: Machine-mode return (treated as a halt in this VM) Mret, + /// SRET: Supervisor-mode return + Sret, /// C.MISC-ALU: compressed miscellaneous ALU operations /// EDUCATIONAL: Compressed miscellaneous ALU operations including C.SUB, C.XOR, C.OR, C.AND. @@ -670,6 +672,8 @@ impl Instruction { "ebreak".to_string(), Instruction::Mret => "mret".to_string(), + Instruction::Sret => + "sret".to_string(), Instruction::Csr { rd, rs1, csr, op, imm } => { let op_str = match op { CsrOp::Csrrw => if *imm { "csrrwi" } else { "csrrw" }, diff --git a/crates/vm/tests/spec_runner.rs b/crates/vm/tests/spec_runner.rs new file mode 100644 index 0000000..980e97d --- /dev/null +++ b/crates/vm/tests/spec_runner.rs @@ -0,0 +1,349 @@ +//! Standalone test runner for rv32ui-p-* ELF files from riscv-tests +//! Loads an ELF file, loads it into the VM, and runs it to completion. + +use std::io::Read; +use std::path::Path; +use vm::memory::{API, MMU, Perms, Sv32Memory, VirtualAddress, PAGE_SIZE}; +use vm::registers::Register; +use vm::vm::VM; + +const DEFAULT_VM_SIZE: usize = 16 * 1024 * 1024; +const STACK_SIZE: usize = 256 * 1024; +const MAX_STEPS: usize = 20_000_000; + +/// Tests that are skipped and the reasons why +const SKIPPED_TESTS: &[(&str, &str)] = &[ + ("fence_i", "Requires self-modifying code support (writes instructions to memory and executes them)"), + ("ld_st", "Contains 64-bit load/store instructions (ld/sd) that the 32-bit VM doesn't support"), + ("st_ld", "Contains 64-bit store/load instructions (sd/ld) that the 32-bit VM doesn't support"), + ("lrsc", "LR/SC implementation needs improvement - causes infinite loops"), +]; + +/// Testing categories to run +const TESTING_CATEGORIES: &[&str] = &["ui", "um", "ua", "uc"]; + +/// Check if a test file should be skipped +fn should_skip_test(file_name: &str) -> Option<&str> { + for (test_name, reason) in SKIPPED_TESTS { + if file_name.ends_with(test_name) { + return Some(reason); + } + } + None +} + +/// Run a single test file +fn run_single_test(elf_path: &str) -> Result<(), Box> { + if !Path::new(elf_path).exists() { + println!("ELF file not found at {}, skipping...", elf_path); + return Ok(()); + } + + // Read ELF file + let mut file = std::fs::File::open(elf_path)?; + let mut elf_bytes = Vec::new(); + file.read_to_end(&mut elf_bytes)?; + + // Parse ELF + let elf = compiler::elf::parse_elf_from_bytes(&elf_bytes)?; + let (code, code_start) = elf.get_flat_code().ok_or("No code section in ELF")?; + let (rodata, rodata_start) = elf.get_flat_rodata().unwrap_or((vec![], u64::MAX)); + let (bss, bss_start) = elf.get_flat_bss().unwrap_or((vec![], u64::MAX)); + + // Get .data section if it exists + let (data, data_start) = if let Some(data_section) = elf.get_section_by_name(".data") { + (data_section.data.to_vec(), data_section.addr as usize) + } else { + (vec![], usize::MAX) + }; + + // Find .tohost section + let tohost_section = if let Some(tohost_section) = elf.get_section_by_name(".tohost") { + println!( + ".tohost section found at addr=0x{:x}, size=0x{:x}", + tohost_section.addr, tohost_section.size + ); + tohost_section + } else { + println!(".tohost section not found, skipping..."); + return Ok(()); + }; + let tohost_addr = tohost_section.addr; + + let mut min_base = code_start as usize; + let mut image_end = (code_start as usize) + code.len(); + + if !rodata.is_empty() { + min_base = min_base.min(rodata_start as usize); + image_end = image_end.max((rodata_start as usize) + rodata.len()); + } + if !data.is_empty() { + min_base = min_base.min(data_start); + image_end = image_end.max(data_start + data.len()); + } + if !bss.is_empty() { + min_base = min_base.min(bss_start as usize); + image_end = image_end.max((bss_start as usize) + bss.len()); + } + let tohost_start = tohost_section.addr as usize; + let tohost_end = tohost_start + (tohost_section.size as usize); + min_base = min_base.min(tohost_start); + image_end = image_end.max(tohost_end); + + let image_size = image_end + .checked_sub(min_base) + .ok_or("invalid image size")?; + let map_len = image_size + STACK_SIZE; + let total_size = map_len.max(DEFAULT_VM_SIZE); + let memory = std::rc::Rc::new(Sv32Memory::new(total_size, PAGE_SIZE)); + + println!("Loading code into VM: addr=0x{:x}, size=0x{:x}", code_start, code.len()); + println!("Mapping {:x}-{:x} (size=0x{:x})", min_base, min_base + map_len, map_len); + memory.map_range(VirtualAddress(min_base as u32), map_len, Perms::rwx_kernel()); + + let mut image = vec![0u8; image_size]; + let code_off = (code_start as usize).saturating_sub(min_base); + image[code_off..code_off + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + let ro_off = (rodata_start as usize).saturating_sub(min_base); + image[ro_off..ro_off + rodata.len()].copy_from_slice(&rodata); + } + if !data.is_empty() { + let data_off = data_start.saturating_sub(min_base); + image[data_off..data_off + data.len()].copy_from_slice(&data); + } + if !bss.is_empty() { + let bss_off = (bss_start as usize).saturating_sub(min_base); + image[bss_off..bss_off + bss.len()].copy_from_slice(&bss); + } + if !tohost_section.data.is_empty() { + let tohost_off = tohost_start.saturating_sub(min_base); + image[tohost_off..tohost_off + tohost_section.data.len()] + .copy_from_slice(tohost_section.data); + } + memory.write_bytes(VirtualAddress(min_base as u32), &image); + + let stack_top = (min_base as u32) + .checked_add(map_len as u32) + .ok_or("stack top overflow")?; + let entry_point = code_start as u32; + + let mut vm = VM::new(memory.clone()); + vm.cpu.verbose = false; + vm.cpu.pc = entry_point; + vm.set_reg_u32(Register::Sp, stack_top); + let root_satp = memory.satp(); + + println!("Running test..."); + let mut steps = 0usize; + loop { + if !vm.cpu.step(memory.clone()) { + break; + } + steps += 1; + if memory.satp() == 0 { + memory.set_satp(root_satp); + } + if steps > MAX_STEPS { + return Err("execution limit reached without tohost signal".into()); + } + let tohost_value = read_tohost_value(memory.as_ref(), tohost_addr)?; + if tohost_value != 0 { + if tohost_value == 1 { + println!("Test completed."); + return Ok(()); + } + return Err(format!("test failed (tohost=0x{:x})", tohost_value).into()); + } + } + + let exit_id = vm.cpu.regs[Register::A7 as usize]; + if exit_id == 93 { + let exit_code = vm.cpu.regs[Register::A0 as usize]; + if exit_code == 0 { + println!("Test completed."); + return Ok(()); + } + return Err(format!("test failed (ecall exit code={})", exit_code).into()); + } + + Err("execution halted without tohost signal".into()) +} + +fn read_tohost_value(memory: &Sv32Memory, tohost_addr: u64) -> Result> { + let addr = u32::try_from(tohost_addr).map_err(|_| "tohost address out of range")?; + let start = VirtualAddress(addr); + let end = start + .checked_add(8) + .ok_or("tohost address overflow")?; + let slice = memory + .mem_slice(start, end) + .ok_or("tohost not mapped")?; + if slice.len() < 8 { + return Err("tohost slice truncated".into()); + } + let bytes: [u8; 8] = slice[0..8].try_into()?; + Ok(u64::from_le_bytes(bytes)) +} + +/// Discover and collect test files for a specific category +fn collect_test_files(test_dir: &str, category: &str) -> (Vec, usize) { + let mut test_files = Vec::new(); + let mut skipped_count = 0; + + println!("Looking for files in: {}", test_dir); + println!("Category prefix: rv32{}p-", category); + + if let Ok(entries) = std::fs::read_dir(test_dir) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(file_name) = path.file_name() { + if let Some(name_str) = file_name.to_str() { + // Include files that start with the category prefix and are not .dump files + let category_prefix = format!("rv32{}-p-", category); + if name_str.starts_with(&category_prefix) && + !path.is_dir() && + !name_str.ends_with(".dump") { + + // Check if this test should be skipped + if let Some(reason) = should_skip_test(name_str) { + println!("Skipping {}: {}", name_str, reason); + skipped_count += 1; + continue; + } + + test_files.push(path.to_string_lossy().to_string()); + } + } + } + } + } + } else { + println!("Failed to read directory: {}", test_dir); + } + + test_files.sort(); // Sort for consistent ordering + (test_files, skipped_count) +} + +/// Run all tests for a specific category +fn run_category_tests(test_dir: &str, category: &str) -> Result<(usize, usize, usize), Box> { + println!("\n=== Running {} category tests ===", category.to_uppercase()); + + let (test_files, skipped_count) = collect_test_files(test_dir, category); + println!("Found {} {} test files to run ({} skipped)", test_files.len(), category, skipped_count); + + let mut passed_count = 0; + let mut failed_count = 0; + + for (i, elf_path) in test_files.iter().enumerate() { + let test_name = std::path::Path::new(elf_path) + .file_name() + .unwrap() + .to_str() + .unwrap(); + + print!("[{:2}/{:2}] {}: ", i + 1, test_files.len(), test_name); + + if let Err(e) = run_single_test(elf_path) { + println!("❌ FAILED - {}", e); + failed_count += 1; + return Err(e); + } else { + println!("✅ PASSED"); + passed_count += 1; + } + } + + println!("=== {} category tests completed ===", category.to_uppercase()); + Ok((passed_count, failed_count, skipped_count)) +} + +#[test] +fn test_riscv_spec() { + // Discover all test files in the riscv-tests directory + let test_dir = "tests/riscv-tests-install/share/riscv-tests/isa"; + + // Print current working directory for debugging + println!("Current dir: {:?}", std::env::current_dir().unwrap()); + println!("Looking for tests in: {}", test_dir); + + // Check if the test directory exists + if !Path::new(test_dir).exists() { + println!("Test directory not found at {}, skipping test", test_dir); + return; + } + + println!("\n🚀 Starting RISC-V Specification Test Suite"); + println!("{}", "=".repeat(60)); + + let mut total_passed = 0; + let mut total_failed = 0; + let mut total_skipped = 0; + let mut category_results = Vec::new(); + + // Run tests for each category + for category in TESTING_CATEGORIES { + match run_category_tests(test_dir, category) { + Ok((passed, failed, skipped)) => { + total_passed += passed; + total_failed += failed; + total_skipped += skipped; + category_results.push((category.to_string(), passed, failed, skipped)); + } + Err(e) => { + println!("❌ Failed to run {} category tests: {}", category, e); + panic!("Test suite failed"); + } + } + } + + // Print comprehensive summary + println!("\n{}", "=".repeat(60)); + println!("📊 RISC-V SPECIFICATION TEST SUITE SUMMARY"); + println!("{}", "=".repeat(60)); + + // Category breakdown + println!("\n📋 Category Breakdown:"); + for (category, passed, failed, skipped) in &category_results { + let total = passed + failed + skipped; + let success_rate = if total > 0 { (*passed as f64 / total as f64) * 100.0 } else { 0.0 }; + println!(" {}: {}/{} passed ({:.1}%) {} skipped", + category.to_uppercase(), passed, total, success_rate, skipped); + } + + // Overall statistics + let total_tests = total_passed + total_failed + total_skipped; + let overall_success_rate = if total_tests > 0 { (total_passed as f64 / total_tests as f64) * 100.0 } else { 0.0 }; + + println!("\n📈 Overall Statistics:"); + println!(" Total Tests: {}", total_tests); + println!(" Passed: {} ✅", total_passed); + println!(" Failed: {} ❌", total_failed); + println!(" Skipped: {} ⏭️", total_skipped); + println!(" Success Rate: {:.1}%", overall_success_rate); + + // Test coverage information + println!("\n🎯 Test Coverage:"); + println!(" UI Tests: Base integer instructions (RV32I)"); + println!(" UM Tests: Integer multiplication and division (RV32M)"); + println!(" UA Tests: Atomic memory operations (RV32A)"); + println!(" UC Tests: Compressed instructions (RV32C)"); + + // Skipped tests explanation + if total_skipped > 0 { + println!("\n⏭️ Skipped Tests:"); + for (test_name, reason) in SKIPPED_TESTS { + println!(" - {}: {}", test_name, reason); + } + } + + println!("\n{}", "=".repeat(60)); + + if total_failed > 0 { + panic!("Test suite completed with {} failures", total_failed); + } else { + println!("🎉 All tests passed successfully!"); + } +} From 6b38d632237c5795cba1fc1ba614d087a3153d9f Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sat, 3 Jan 2026 21:28:35 +0200 Subject: [PATCH 68/70] aTester: support multiple input buffers --- aTester/src/runners/avm.rs | 44 +++++++++++++++++++++++++++++++------- aTester/src/types.rs | 2 +- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/aTester/src/runners/avm.rs b/aTester/src/runners/avm.rs index ae745cc..52919a3 100644 --- a/aTester/src/runners/avm.rs +++ b/aTester/src/runners/avm.rs @@ -38,11 +38,23 @@ impl ArchRunner for AvmRunner { let heap_ptr = Rc::new(Cell::new(0u32)); let entry_point = load_kernel(&elf_bytes, &memory, heap_ptr.as_ref())?; - let input_ptr = if options.input.is_empty() { - 0 - } else { - alloc_on_heap(memory.as_ref(), heap_ptr.as_ref(), &options.input) - }; + if options.input.len() > 3usize { + return Err(RunError { + message: format!("too many inputs ({}); max is 3", options.input.len()), + }); + } + let mut input_ptrs = [0u32; 3]; + let mut input_lens = [0u32; 3]; + for idx in 0..options.input.len() { + let bytes = options + .input + .get(idx) + .map(|input| input.as_slice()) + .unwrap_or(&[]); + let ptr = alloc_on_heap(memory.as_ref(), heap_ptr.as_ref(), bytes); + input_ptrs[idx] = ptr; + input_lens[idx] = bytes.len() as u32; + } let boot_info_ptr = place_boot_info(memory.as_ref(), heap_ptr.as_ref(), total_size)?; let mut vm = VM::new(memory.clone()); @@ -52,9 +64,25 @@ impl ArchRunner for AvmRunner { let writer = Rc::new(RefCell::new(StringWriter::default())); vm.cpu.set_verbose_writer(writer.clone()); vm.cpu.pc = entry_point; - vm.set_reg_u32(Register::A0, input_ptr); - vm.set_reg_u32(Register::A1, options.input.len() as u32); - vm.set_reg_u32(Register::A2, boot_info_ptr); + + // set input regs + const ARG_REGS: [Register; 8] = [ + Register::A0, + Register::A1, + Register::A2, + Register::A3, + Register::A4, + Register::A5, + Register::A6, + Register::A7, + ]; + for (idx, ptr) in input_ptrs.iter().enumerate() { + let reg_idx = idx * 2; + vm.set_reg_u32(ARG_REGS[reg_idx], *ptr); + vm.set_reg_u32(ARG_REGS[reg_idx + 1], input_lens[idx]); + } + vm.set_reg_u32(Register::A6, boot_info_ptr); + vm.set_reg_u32(Register::A7, 0); vm.raw_run(); diff --git a/aTester/src/types.rs b/aTester/src/types.rs index 38ce1a2..215e3f5 100644 --- a/aTester/src/types.rs +++ b/aTester/src/types.rs @@ -10,7 +10,7 @@ pub struct RunOptions { pub timeout_ms: Option, pub vm_memory_size: Option, pub verbose: bool, - pub input: Vec, + pub input: Vec>, } #[derive(Debug, Clone)] From 88c65690bc998af8b5c38329d5df0ac657e74ae6 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 4 Jan 2026 19:17:52 +0200 Subject: [PATCH 69/70] add fixtures-based example suite and route run_examples through it add aTester examples test harness with fixtures, bundles, and expected results pass kernel output bytes through RunResult for test-side decoding refactor kernel test evaluation to decode TestResults from output bytes move example integration tests out of crates/examples and clean its dev deps update run_examples make target and examples README to use aTester align kernel test results address with KERNEL_RESULT_ADDR --- Cargo.lock | 63 +- Makefile | 4 +- aTester/Cargo.toml | 3 + aTester/src/arch.rs | 1 + aTester/src/runners/avm.rs | 62 +- aTester/tests/examples.rs | 180 ++++++ aTester/tests/fixtures/examples.rs | 612 ++++++++++++++++++ aTester/tests/kernel.rs | 35 +- crates/examples/Cargo.toml | 8 - crates/examples/README.md | 29 +- .../examples/tests/binary_comparison_test.rs | 335 ---------- crates/examples/tests/common/ecdsa.rs | 40 -- crates/examples/tests/common/router.rs | 23 - crates/examples/tests/common/state.rs | 19 - crates/examples/tests/common/test_runner.rs | 299 --------- crates/examples/tests/common/utils.rs | 147 ----- crates/examples/tests/ecdsa_payload_test.rs | 39 -- crates/examples/tests/examples_test.rs | 541 ---------------- crates/kernel/src/tests/results.rs | 4 +- 19 files changed, 875 insertions(+), 1569 deletions(-) create mode 100644 aTester/tests/examples.rs create mode 100644 aTester/tests/fixtures/examples.rs delete mode 100644 crates/examples/tests/binary_comparison_test.rs delete mode 100644 crates/examples/tests/common/ecdsa.rs delete mode 100644 crates/examples/tests/common/router.rs delete mode 100644 crates/examples/tests/common/state.rs delete mode 100644 crates/examples/tests/common/test_runner.rs delete mode 100644 crates/examples/tests/common/utils.rs delete mode 100644 crates/examples/tests/ecdsa_payload_test.rs delete mode 100644 crates/examples/tests/examples_test.rs diff --git a/Cargo.lock b/Cargo.lock index 7a42c69..4efb36c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ version = "0.1.0" dependencies = [ "compiler", "goblin 0.8.2", + "state", "types", "vm", ] @@ -162,15 +163,9 @@ dependencies = [ name = "examples" version = "0.1.0" dependencies = [ - "bootloader", "clibc", - "compiler", "k256", - "once_cell", - "serde_json", "sha2", - "state", - "types", ] [[package]] @@ -236,12 +231,6 @@ dependencies = [ "digest", ] -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - [[package]] name = "k256" version = "0.13.4" @@ -275,18 +264,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - [[package]] name = "pkcs8" version = "0.10.2" @@ -337,12 +314,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "scroll" version = "0.12.0" @@ -397,38 +368,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.142" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - [[package]] name = "sha2" version = "0.10.9" diff --git a/Makefile b/Makefile index 6532543..ff87ed7 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,8 @@ run_examples: @echo "=== Building example programs ===" RUSTFLAGS="-Awarnings" $(MAKE) -C crates/examples @$(MAKE) kernel - @echo "=== Running example crate tests ===" - cd crates/examples && RUSTFLAGS="-Awarnings" KERNEL_ELF="../bootloader/bin/kernel.elf" cargo test --test examples_test -- --nocapture + @echo "=== Running aTester example tests ===" + cargo test -p a_tests --test examples -- --nocapture @echo "=== Example programs build and tests complete ===" clean: diff --git a/aTester/Cargo.toml b/aTester/Cargo.toml index 171cfac..19bdc3b 100644 --- a/aTester/Cargo.toml +++ b/aTester/Cargo.toml @@ -11,3 +11,6 @@ compiler = { path = "../crates/compiler" } goblin = "0.8" types = { path = "../crates/types" } vm = { path = "../crates/vm" } + +[dev-dependencies] +state = { path = "../crates/state" } diff --git a/aTester/src/arch.rs b/aTester/src/arch.rs index b43dc66..1a73ac3 100644 --- a/aTester/src/arch.rs +++ b/aTester/src/arch.rs @@ -7,6 +7,7 @@ pub struct RunResult { pub exit_code: i32, pub stdout: String, pub stderr: String, + pub output: Vec, } #[derive(Debug)] diff --git a/aTester/src/runners/avm.rs b/aTester/src/runners/avm.rs index 52919a3..81036af 100644 --- a/aTester/src/runners/avm.rs +++ b/aTester/src/runners/avm.rs @@ -7,6 +7,7 @@ use std::rc::Rc; use compiler::elf::parse_elf_from_bytes; use goblin::elf::Elf; use types::boot::BootInfo; +use types::kernel_result::KERNEL_RESULT_ADDR; use types::SV32_DIRECT_MAP_BASE; use vm::memory::{API, MMU, Perms, Sv32Memory, VirtualAddress, HEAP_PTR_OFFSET, PAGE_SIZE}; use vm::registers::Register; @@ -81,60 +82,36 @@ impl ArchRunner for AvmRunner { vm.set_reg_u32(ARG_REGS[reg_idx], *ptr); vm.set_reg_u32(ARG_REGS[reg_idx + 1], input_lens[idx]); } - vm.set_reg_u32(Register::A6, boot_info_ptr); - vm.set_reg_u32(Register::A7, 0); + let boot_reg_idx = options.input.len() * 2; + if boot_reg_idx >= ARG_REGS.len() { + return Err(RunError { + message: "no argument register available for boot info".to_string(), + }); + } + vm.set_reg_u32(ARG_REGS[boot_reg_idx], boot_info_ptr); + if boot_reg_idx + 1 < ARG_REGS.len() { + vm.set_reg_u32(ARG_REGS[boot_reg_idx + 1], 0); + } vm.raw_run(); let stdout = writer.borrow().buffer.clone(); - let results = read_test_results(memory.as_ref())?; - let exit_code = if results.status == 0 { 0 } else { 1 }; - let stderr = if results.status == 0 { - String::new() - } else { - format!("test failed: {}", results.detail) - }; + let output = read_kernel_blob(memory.as_ref()).unwrap_or_default(); + let exit_code = 0; + let stderr = String::new(); Ok(RunResult { exit_code, stdout, stderr, + output, }) } } const KERNEL_WINDOW_BYTES: usize = 4 * 1024 * 1024; const KERNEL_STACK_TOP: u32 = KERNEL_WINDOW_BYTES as u32; -const TEST_RESULTS_ADDR: u32 = 0x0003_f000; - -#[repr(C)] -struct TestResults { - status: u32, - detail: u32, -} - -fn read_test_results(memory: &Sv32Memory) -> Result { - let start = VirtualAddress(TEST_RESULTS_ADDR); - let end = start - .checked_add(mem::size_of::() as u32) - .ok_or_else(|| RunError { - message: "test results overflow".to_string(), - })?; - let slice = memory - .mem_slice(start, end) - .ok_or_else(|| RunError { - message: "test results not found".to_string(), - })?; - let bytes = slice.as_ref(); - if bytes.len() < mem::size_of::() { - return Err(RunError { - message: "test results truncated".to_string(), - }); - } - let status = u32::from_le_bytes(bytes[0..4].try_into().unwrap()); - let detail = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); - Ok(TestResults { status, detail }) -} +const KERNEL_RESULT_DUMP_BYTES: u32 = 1024 * 1024; fn load_kernel( elf_bytes: &[u8], @@ -231,6 +208,13 @@ fn load_kernel( Ok(entry_point) } +fn read_kernel_blob(memory: &Sv32Memory) -> Option> { + let start = VirtualAddress(KERNEL_RESULT_ADDR); + let end = start.checked_add(KERNEL_RESULT_DUMP_BYTES)?; + let slice = memory.mem_slice(start, end)?; + Some(slice.as_ref().to_vec()) +} + fn place_boot_info(memory: &Sv32Memory, heap_ptr: &Cell, memory_size: usize) -> Result { let heap_start = ensure_heap_ptr(heap_ptr); let aligned_heap = (heap_start + 7) & !7; diff --git a/aTester/tests/examples.rs b/aTester/tests/examples.rs new file mode 100644 index 0000000..9a610cf --- /dev/null +++ b/aTester/tests/examples.rs @@ -0,0 +1,180 @@ +use std::path::{Path, PathBuf}; + +use a_tests::{AvmRunner, RunOptions, Suite, TestCase, TestEvaluator, TestKind, TestOutcome}; +use types::TransactionReceipt; + +#[path = "fixtures/examples.rs"] +mod fixtures; + +use fixtures::{all_example_cases, expected_for, test_state_bytes}; + +struct ExampleEvaluator; + +impl TestEvaluator for ExampleEvaluator { + fn evaluate(&self, case: &TestCase, result: &a_tests::RunResult) -> TestOutcome { + let receipts_slice = match kernel_receipts_slice(&result.output) { + Some(slice) => slice, + None => return TestOutcome::Failed("kernel receipts not in dump".to_string()), + }; + let receipts = match TransactionReceipt::decode_list(receipts_slice) { + Some(receipts) => receipts, + None => return TestOutcome::Failed("failed to decode receipts".to_string()), + }; + let receipt = match receipts.last() { + Some(receipt) => receipt, + None => return TestOutcome::Failed("missing transaction receipt".to_string()), + }; + let expected = match expected_for(case.name.as_str()) { + Some(expected) => expected, + None => { + return TestOutcome::Failed(format!( + "missing expected result for {}", + case.name + )) + } + }; + let success = receipt.result.success; + let error_code = receipt.result.error_code; + let data_len = receipt.result.data_len; + let data = receipt.result.data; + if success != expected.success { + return TestOutcome::Failed(format!( + "expected success={}, got {}", + expected.success, success + )); + } + if error_code != expected.error_code { + return TestOutcome::Failed(format!( + "expected error_code={}, got {}", + expected.error_code, error_code + )); + } + let data_len = data_len as usize; + let actual = &data[..data_len.min(data.len())]; + if actual != expected.data.as_slice() { + return TestOutcome::Failed(format!( + "expected data {:?}, got {:?}", + expected.data, actual + )); + } + TestOutcome::Passed + } +} + +#[test] +fn examples_tests() { + build_kernel().expect("failed to build kernel"); + build_examples().expect("failed to build example programs"); + + let target_dir = kernel_elf_dir(); + let state_bytes = test_state_bytes(); + let cases = all_example_cases() + .expect("failed to build example bundles") + .into_iter() + .map(|case| TestCase { + name: case.name.to_string(), + kind: TestKind::Smoke, + elf: target_dir.join("kernel.elf"), + options: RunOptions { + timeout_ms: None, + vm_memory_size: None, + verbose: false, + input: vec![case.bundle.encode(), state_bytes.clone()], + }, + }) + .collect::>(); + + let evaluator = ExampleEvaluator; + let suite = Suite { + name: "examples_tests".to_string(), + cases, + evaluator: &evaluator, + }; + + let runner = AvmRunner::new(); + let reports = suite.run(&runner); + + for report in &reports { + if !report.stdout.is_empty() { + println!("--- {} stdout ---\n{}", report.name, report.stdout); + } + if !report.stderr.is_empty() { + eprintln!("--- {} stderr ---\n{}", report.name, report.stderr); + } + } + + let failures: Vec<_> = reports + .iter() + .filter(|report| matches!(report.outcome, TestOutcome::Failed(_))) + .collect(); + + if !failures.is_empty() { + let mut details = String::new(); + for report in failures { + if let TestOutcome::Failed(detail) = &report.outcome { + details.push_str(&format!("{}: {}\n", report.name, detail)); + } + } + panic!("example test failures:\n{}", details); + } +} + +fn kernel_elf_dir() -> PathBuf { + std::env::var("KERNEL_ELF_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| workspace_root().join("crates/bootloader/bin")) +} + +fn build_kernel() -> Result<(), String> { + let status = std::process::Command::new("make") + .args(["kernel"]) + .current_dir(workspace_root()) + .status() + .map_err(|e| format!("failed to spawn kernel make: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("kernel build failed with status: {status}")) + } +} + +fn build_examples() -> Result<(), String> { + let status = std::process::Command::new("make") + .args(["-C", "crates/examples"]) + .current_dir(workspace_root()) + .status() + .map_err(|e| format!("failed to spawn examples make: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("examples build failed with status: {status}")) + } +} + +fn workspace_root() -> PathBuf { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .map(PathBuf::from) + .expect("missing workspace root") +} + +fn kernel_receipts_slice(dump: &[u8]) -> Option<&[u8]> { + if dump.len() < 16 { + return None; + } + let receipts_ptr = u32::from_le_bytes(dump[0..4].try_into().ok()?); + let receipts_len = u32::from_le_bytes(dump[4..8].try_into().ok()?); + if receipts_ptr == 0 || receipts_len == 0 { + return None; + } + let base = types::kernel_result::KERNEL_RESULT_ADDR; + let start = receipts_ptr.checked_sub(base)? as usize; + let end = start.checked_add(receipts_len as usize)?; + if end > dump.len() { + return None; + } + Some(&dump[start..end]) +} diff --git a/aTester/tests/fixtures/examples.rs b/aTester/tests/fixtures/examples.rs new file mode 100644 index 0000000..e295fad --- /dev/null +++ b/aTester/tests/fixtures/examples.rs @@ -0,0 +1,612 @@ +use compiler::elf::parse_elf_from_bytes; +use state; +use types::address::Address; +use types::transaction::{Transaction, TransactionBundle, TransactionType}; + +pub struct ExpectedResult { + pub success: bool, + pub error_code: u32, + pub data: Vec, +} + +pub struct ExampleCase { + pub name: &'static str, + pub description: &'static str, + pub bundle: TransactionBundle, +} + +pub fn test_state_bytes() -> Vec { + let mut state = state::State::new(); + for addr_hex in [ + "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2", + "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3", + ] { + let addr = to_address(addr_hex); + let account = state.get_account_mut(&addr); + account.balance = 1_000_000_000u128; + } + state.encode() +} + +pub fn all_example_cases() -> Result, String> { + Ok(vec![ + ExampleCase { + name: "erc20", + description: "ERC-20 init, transfer, and balance query flow", + bundle: build_erc20_bundle()?, + }, + ExampleCase { + name: "call program", + description: "Cross-contract call with nested program execution", + bundle: build_call_program_bundle()?, + }, + ExampleCase { + name: "account create (storage)", + description: "Create a contract and invoke a storage call", + bundle: build_account_create_storage_bundle()?, + }, + ExampleCase { + name: "account create (simple)", + description: "Create a simple contract and verify return data", + bundle: build_account_create_simple_bundle()?, + }, + ExampleCase { + name: "multi function (simple)", + description: "Router-style call into a multi-function contract", + bundle: build_multi_function_simple_bundle()?, + }, + ExampleCase { + name: "allocator demo", + description: "Heap allocation and collection usage in guest code", + bundle: build_allocator_demo_bundle()?, + }, + ExampleCase { + name: "native transfer", + description: "Native value transfer without a contract call", + bundle: build_native_transfer_bundle(), + }, + ExampleCase { + name: "guest transfer syscall", + description: "Program issues a native transfer syscall", + bundle: build_guest_transfer_syscall_bundle()?, + }, + ExampleCase { + name: "dex amm", + description: "AMM lifecycle: init, approve, add/remove liquidity, swap", + bundle: build_dex_amm_bundle()?, + }, + ExampleCase { + name: "ecdsa verify", + description: "ECDSA signature verification within the VM", + bundle: build_ecdsa_verify_bundle()?, + }, + ]) +} + +pub fn expected_for(name: &str) -> Option { + match name { + "erc20" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![128, 240, 250, 2], + }), + "call program" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![100, 0, 0, 0], + }), + "account create (storage)" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + "account create (simple)" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![100, 0, 0, 0], + }), + "multi function (simple)" => Some(ExpectedResult { + success: true, + error_code: 0, + data: vec![100, 0, 0, 0], + }), + "allocator demo" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + "native transfer" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + "guest transfer syscall" => Some(ExpectedResult { + success: true, + error_code: 0, + data: 42u128.to_le_bytes().to_vec(), + }), + "dex amm" => { + let mut buf = Vec::new(); + buf.extend_from_slice(&101000u128.to_le_bytes()); + buf.extend_from_slice(&495050u128.to_le_bytes()); + Some(ExpectedResult { + success: true, + error_code: 0, + data: buf, + }) + } + "ecdsa verify" => Some(ExpectedResult { + success: true, + error_code: 0, + data: Vec::new(), + }), + _ => None, + } +} + +struct HostFuncCall { + selector: u8, + args: Vec, +} + +fn encode_router_calls(calls: &[HostFuncCall]) -> Vec { + let mut encoded = Vec::new(); + for call in calls { + let len = call.args.len(); + assert!(len <= 255, "argument too long for 1-byte length field"); + encoded.push(call.selector); + encoded.push(len as u8); + encoded.extend_from_slice(&call.args); + } + encoded +} + +fn build_erc20_bundle() -> Result { + let deployer = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + let contract = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + from: deployer, + to: contract, + data: get_program_code("erc20")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: contract, + from: deployer, + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: (|| { + let max_supply: u32 = 100000000; + let mut max_supply_bytes = max_supply.to_le_bytes().to_vec(); + max_supply_bytes.extend(vec![18u8]); + max_supply_bytes + })(), + }]), + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: contract, + from: deployer, + data: encode_router_calls(&[HostFuncCall { + selector: 0x02, + args: (|| { + let to_addr = + to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); + let mut args = to_addr.0.to_vec(); + let amount: u32 = 50000000; + args.extend(amount.to_le_bytes()); + args + })(), + }]), + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: contract, + from: deployer, + data: encode_router_calls(&[HostFuncCall { + selector: 0x05, + args: (|| { + let owner = + to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + owner.0.to_vec() + })(), + }]), + value: 0, + nonce: 0, + }, + ])) +} + +fn build_call_program_bundle() -> Result { + let caller = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + let callee = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + from: caller, + to: caller, + data: get_program_code("call_program")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::CreateAccount, + from: caller, + to: callee, + data: get_program_code("simple")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: caller, + from: caller, + data: (|| { + let mut data = callee.0.to_vec(); + data.extend(vec![100, 0, 0, 0, 42, 0, 0, 0]); + data + })(), + value: 0, + nonce: 0, + }, + ])) +} + +fn build_account_create_storage_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("storage")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: vec![], + value: 0, + nonce: 0, + }, + ])) +} + +fn build_account_create_simple_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("simple")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: vec![100, 0, 0, 0, 42, 0, 0, 0], + value: 0, + nonce: 0, + }, + ])) +} + +fn build_multi_function_simple_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("multi_func")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: vec![100, 0, 0, 0, 42, 0, 0, 0], + }]), + value: 0, + nonce: 0, + }, + ])) +} + +fn build_allocator_demo_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("allocator_demo")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: vec![ + 12, 0, 0, 0, 15, 0, 0, 0, 100, 0, 0, 0, 95, 0, 0, 0, 87, 0, 0, 0, 92, 0, + 0, 0, + ], + value: 0, + nonce: 0, + }, + ])) +} + +fn build_native_transfer_bundle() -> TransactionBundle { + TransactionBundle::new(vec![Transaction { + tx_type: TransactionType::Transfer, + to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), + from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), + data: vec![], + value: 10, + nonce: 0, + }]) +} + +fn build_guest_transfer_syscall_bundle() -> Result { + let program = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"); + let sender = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"); + let recipient = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: program, + from: sender, + data: get_program_code("native_transfer")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: program, + from: sender, + data: (|| { + let mut data = recipient.0.to_vec(); + data.extend_from_slice(&42u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 1, + }, + ])) +} + +fn build_dex_amm_bundle() -> Result { + let erc20 = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"); + let dex = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"); + let user2 = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); + let user3 = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: erc20, + from: user3, + data: get_program_code("erc20")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: erc20, + from: user3, + data: encode_router_calls(&[HostFuncCall { + selector: 0x01, + args: (|| { + let mut args = Vec::new(); + let supply: u32 = 1_000_000; + args.extend_from_slice(&supply.to_le_bytes()); + args.push(0); + args + })(), + }]), + value: 0, + nonce: 1, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: erc20, + from: user3, + data: encode_router_calls(&[HostFuncCall { + selector: 0x03, + args: (|| { + let mut args = dex.0.to_vec(); + let amount: u32 = 500_000; + args.extend_from_slice(&amount.to_le_bytes()); + args + })(), + }]), + value: 0, + nonce: 2, + }, + Transaction { + tx_type: TransactionType::CreateAccount, + to: dex, + from: user3, + data: get_program_code("dex")?, + value: 0, + nonce: 3, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: dex, + from: user3, + data: (|| { + let mut data = Vec::new(); + data.push(0x01); + data.extend_from_slice(&100_000u64.to_le_bytes()); + data.extend_from_slice(&500_000u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 4, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: dex, + from: user2, + data: (|| { + let mut data = Vec::new(); + data.push(0x03); + data.push(0x00); + data.extend_from_slice(&1_000u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: dex, + from: user3, + data: (|| { + let mut data = Vec::new(); + data.push(0x02); + data.extend_from_slice(&100_000u64.to_le_bytes()); + data + })(), + value: 0, + nonce: 5, + }, + ])) +} + +fn build_ecdsa_verify_bundle() -> Result { + let addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); + Ok(TransactionBundle::new(vec![ + Transaction { + tx_type: TransactionType::CreateAccount, + to: addr, + from: addr, + data: get_program_code("ecdsa_verify")?, + value: 0, + nonce: 0, + }, + Transaction { + tx_type: TransactionType::ProgramCall, + to: addr, + from: addr, + data: build_ecdsa_payload(), + value: 0, + nonce: 1, + }, + ])) +} + +fn build_ecdsa_payload() -> Vec { + let mut payload = Vec::with_capacity(1 + ECDSA_PK_BYTES.len() + ECDSA_SIG_BYTES.len() + ECDSA_HASH.len()); + payload.push(ECDSA_PK_BYTES.len() as u8); + payload.extend_from_slice(&ECDSA_PK_BYTES); + payload.extend_from_slice(&ECDSA_SIG_BYTES); + payload.extend_from_slice(&ECDSA_HASH); + payload +} + +const ECDSA_HASH: [u8; 32] = [ + 0x3b, 0xbd, 0x38, 0x9e, 0x94, 0x1c, 0x63, 0x7f, 0x36, 0x32, 0xaa, 0xf4, 0x2f, 0x93, 0xb7, 0xb1, + 0xf1, 0x7c, 0x6f, 0x31, 0x86, 0x92, 0x01, 0x34, 0x1d, 0x5f, 0x28, 0x40, 0x61, 0x5c, 0xac, 0x2b, +]; +const ECDSA_PK_BYTES: [u8; 33] = [ + 0x02, 0xda, 0x8c, 0x8e, 0x0a, 0x4e, 0x5d, 0xfc, 0x76, 0x6f, 0xf1, 0xcb, 0xda, 0x27, 0x03, 0xea, + 0xcd, 0xb0, 0xdf, 0x07, 0xda, 0x19, 0xde, 0x65, 0x03, 0x51, 0x46, 0xdb, 0x9b, 0x9c, 0x8a, 0xb7, + 0x0c, +]; +const ECDSA_SIG_BYTES: [u8; 64] = [ + 0x13, 0xe3, 0x22, 0xb9, 0x33, 0x19, 0x17, 0x76, 0x6d, 0x8c, 0xbf, 0xe9, 0x9f, 0x1d, 0x44, 0xd8, + 0xeb, 0x4f, 0x1d, 0xb3, 0xca, 0xd1, 0x31, 0xaf, 0x92, 0xb2, 0xf2, 0x26, 0x3c, 0xe6, 0x60, 0x92, + 0x2a, 0x3a, 0xef, 0x94, 0xe6, 0x3e, 0x74, 0x06, 0xf4, 0x20, 0xee, 0x0c, 0x0c, 0xb6, 0x5f, 0xce, + 0xe0, 0x45, 0x26, 0xba, 0x9e, 0x36, 0xf6, 0x20, 0x92, 0x77, 0x73, 0x9d, 0x2d, 0x64, 0x37, 0xa2, +]; + +fn get_program_code(name: &str) -> Result, String> { + let bytes = read_example_bin(name)?; + let elf = parse_elf_from_bytes(&bytes) + .map_err(|e| format!("failed to parse elf for {name}: {e}"))?; + + let (code, code_start) = elf + .get_flat_code() + .ok_or_else(|| format!("no code section for {name}"))?; + let (rodata, rodata_start) = elf.get_flat_rodata().unwrap_or((Vec::new(), u64::MAX)); + + let mut total_len = code_start + code.len() as u64; + if !rodata.is_empty() { + total_len = rodata_start + rodata.len() as u64; + } + + let mut combined = vec![0u8; total_len as usize]; + combined[code_start as usize..code_start as usize + code.len()].copy_from_slice(&code); + if !rodata.is_empty() { + combined[rodata_start as usize..rodata_start as usize + rodata.len()] + .copy_from_slice(&rodata); + } + Ok(combined) +} + +fn read_example_bin(name: &str) -> Result, String> { + let base = workspace_root().join("crates/examples/bin"); + let mut candidates = vec![base.join(name), base.join(format!("{name}.elf"))]; + candidates.push( + workspace_root() + .join("target/avm32/release") + .join(name), + ); + + for path in candidates { + if path.exists() { + return std::fs::read(&path) + .map_err(|e| format!("failed to read {}: {e}", path.display())); + } + } + Err(format!( + "missing example binary for {name}; try running make -C crates/examples" + )) +} + +fn to_address(hex: &str) -> Address { + assert!(hex.len() == 40, "hex string must be 40 characters"); + fn from_hex_char(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => panic!("invalid hex character"), + } + } + let mut bytes = [0u8; 20]; + let hex_bytes = hex.as_bytes(); + for i in 0..20 { + let hi = from_hex_char(hex_bytes[i * 2]); + let lo = from_hex_char(hex_bytes[i * 2 + 1]); + bytes[i] = (hi << 4) | lo; + } + Address(bytes) +} + +fn workspace_root() -> std::path::PathBuf { + let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .map(std::path::PathBuf::from) + .expect("missing workspace root") +} diff --git a/aTester/tests/kernel.rs b/aTester/tests/kernel.rs index 88419ed..f289fd9 100644 --- a/aTester/tests/kernel.rs +++ b/aTester/tests/kernel.rs @@ -7,15 +7,18 @@ struct ExitCodeEvaluator; impl TestEvaluator for ExitCodeEvaluator { fn evaluate(&self, case: &TestCase, result: &a_tests::RunResult) -> TestOutcome { - if result.exit_code == 0 { - TestOutcome::Passed - } else { - let detail = if result.stderr.is_empty() { - format!("{} failed with exit code {}", case.name, result.exit_code) - } else { - result.stderr.clone() - }; - TestOutcome::Failed(detail) + match read_test_results_from_output(&result.output) { + Ok(results) => { + if results.status == 0 { + TestOutcome::Passed + } else { + TestOutcome::Failed(format!( + "{} failed with detail {}", + case.name, results.detail + )) + } + } + Err(err) => TestOutcome::Failed(format!("{} failed: {}", case.name, err)), } } } @@ -82,6 +85,20 @@ fn kernel_tests() { } } +struct TestResults { + status: u32, + detail: u32, +} + +fn read_test_results_from_output(output: &[u8]) -> Result { + if output.len() < 8 { + return Err("missing test results output".to_string()); + } + let status = u32::from_le_bytes(output[0..4].try_into().unwrap()); + let detail = u32::from_le_bytes(output[4..8].try_into().unwrap()); + Ok(TestResults { status, detail }) +} + fn kernel_bins() -> Result, String> { let manifest_path = workspace_root().join("crates/kernel/Cargo.toml"); let contents = fs::read_to_string(&manifest_path) diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 25c50b2..fed7b87 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -8,14 +8,6 @@ clibc = { path = "../clibc", features = ["guest"] } sha2 = { version = "0.10", default-features = false } k256 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdsa", "alloc"] } -[dev-dependencies] -types = { path = "../types" } # adjust path as needed -compiler = { path = "../compiler" } # adjust path as needed -bootloader = { path = "../bootloader" } -once_cell = "1.19.0" -serde_json = "1.0" -state = { path = "../state" } - [features] default = [] binaries = [] # Enable this feature to build RISC-V binaries diff --git a/crates/examples/README.md b/crates/examples/README.md index 640b842..4f6d807 100644 --- a/crates/examples/README.md +++ b/crates/examples/README.md @@ -58,6 +58,30 @@ Demonstrates how one contract can call another contract. - **Features**: External contract calls, result handling - **Use cases**: DeFi protocols, modular contract systems +### 8. **logging.rs** - Logging and Tracing +Emits structured logs from inside the VM. +- **Purpose**: Show runtime logging patterns +- **Features**: Kernel log output, string formatting +- **Use cases**: Debugging contract behavior + +### 9. **native_transfer.rs** - Native Transfer Syscall +Uses the transfer syscall from a guest program. +- **Purpose**: Demonstrate native token transfers +- **Features**: Syscall usage, parameter encoding +- **Use cases**: Simple payments, account funding + +### 10. **dex.rs** - Simple AMM +Implements a basic AMM between native AM and an ERC-20 token. +- **Purpose**: Show multi-contract interactions and liquidity flows +- **Features**: Add/remove liquidity, swaps +- **Use cases**: DeFi primitives + +### 11. **ecdsa_verify.rs** - Signature Verification +Verifies an ECDSA signature inside the VM. +- **Purpose**: Demonstrate cryptographic verification +- **Features**: Signature parsing, hashing, verification +- **Use cases**: Auth, permit-style flows + ## Project Structure ``` @@ -67,7 +91,6 @@ examples/ │ ├── *.elf # RISC-V ELF binaries │ ├── *.abi.json # Contract ABI definitions │ └── *_client.rs # Auto-generated client code -├── tests/ # Integration tests ├── Makefile # Build system ├── Cargo.toml # Rust dependencies └── generate_abis.sh # ABI generation script @@ -75,9 +98,9 @@ examples/ ## Running Tests -To run the integration tests for all examples: +Example integration tests run through the `aTester` crate in the workspace: ```bash -cargo test +make run_examples ``` ## Key Concepts Demonstrated diff --git a/crates/examples/tests/binary_comparison_test.rs b/crates/examples/tests/binary_comparison_test.rs deleted file mode 100644 index 82df208..0000000 --- a/crates/examples/tests/binary_comparison_test.rs +++ /dev/null @@ -1,335 +0,0 @@ -use core::cell::RefCell; -use core::fmt::Write; -use std::fs; -use std::path::Path; -use std::rc::Rc; - -// Import the test runner and related modules -#[path = "examples_test.rs"] -mod examples_test; - -#[path = "common/utils.rs"] -mod utils; - -use examples_test::TestRunner; -use examples_test::build_ecdsa_payload; -use k256::ecdsa::{Signature, VerifyingKey, signature::hazmat::PrehashVerifier}; - -#[test] -fn test_vm_binary_comparison() -> Result<(), String> { - println!("\n=== VM Binary Comparison Test ===\n"); - - // Step 1: Create TestRunner with file output - let vm_log_path = "/tmp/vm_binary_comparison.log"; - println!( - "Step 1: Running TestRunner with file output to: {}", - vm_log_path - ); - - // Create a file writer for the TestRunner - let file = - fs::File::create(vm_log_path).map_err(|e| format!("Failed to create log file: {}", e))?; - - // Create a Write adapter for the file - struct FileWriter(fs::File); - - impl Write for FileWriter { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - use std::io::Write; - self.0 - .write_all(s.as_bytes()) - .map_err(|_| core::fmt::Error)?; - self.0.flush().map_err(|_| core::fmt::Error)?; - Ok(()) - } - } - - let writer: Rc> = Rc::new(RefCell::new(FileWriter(file))); - - // Create TestRunner with file output and verbose mode for instruction tracing - let runner = TestRunner::with_writer(writer) - .with_verbose(true) // Enable verbose mode for PC traces - .with_memory_size(512 * 1024); // Larger memory for crypto-heavy binaries - - // Run all test cases - runner.execute()?; - - println!("✅ TestRunner execution completed"); - - // Step 2: Verify the log file was created - let log_size = fs::metadata(vm_log_path) - .map_err(|e| format!("Failed to read log file metadata: {}", e))? - .len(); - - println!("Step 2: VM log file created, size: {} bytes", log_size); - - // Step 3: Parse the log to extract test cases and instructions - let log_content = - fs::read_to_string(vm_log_path).map_err(|e| format!("Failed to read log file: {}", e))?; - - let test_cases = extract_test_cases(&log_content); - println!( - "\nStep 3: Extracted {} test cases from log", - test_cases.len() - ); - - // Step 4: Check for corresponding ELF binaries - let binaries_dir = Path::new("../../target/avm32/release"); - - println!( - "\nStep 4: Checking for ELF binaries in: {}", - binaries_dir.display() - ); - - let mut comparison_results = Vec::new(); - - for test_case in &test_cases { - println!("\n Processing test case: {}", test_case.name); - - // Extract binary mappings - for (address, binary_name) in &test_case.address_mappings { - println!(" Address {} -> Binary: {}", address, binary_name); - - let elf_path = binaries_dir.join(binary_name); - - if elf_path.exists() { - println!(" ✅ ELF found: {}", elf_path.display()); - - // Here we would run the actual comparison - // For this test, we'll just verify the structure - let result = ComparisonResult { - test_name: test_case.name.clone(), - binary_name: binary_name.clone(), - vm_instructions: test_case.instructions.len(), - elf_found: true, - match_percentage: calculate_match_percentage(&test_case.instructions), - }; - - comparison_results.push(result); - } else { - println!(" ⚠️ ELF not found: {}", elf_path.display()); - - let result = ComparisonResult { - test_name: test_case.name.clone(), - binary_name: binary_name.clone(), - vm_instructions: test_case.instructions.len(), - elf_found: false, - match_percentage: 0.0, - }; - - comparison_results.push(result); - } - } - } - - // Step 5: Generate summary report - println!("\n{}", "=".repeat(50)); - println!("COMPARISON SUMMARY"); - println!("{}", "=".repeat(50)); - - let mut all_100_percent = true; - let mut total_instructions = 0; - - for result in &comparison_results { - println!("\n📊 {} ({})", result.test_name, result.binary_name); - println!(" VM Instructions: {}", result.vm_instructions); - println!( - " ELF Found: {}", - if result.elf_found { "Yes" } else { "No" } - ); - - if result.elf_found { - println!(" Match: {:.1}%", result.match_percentage); - - if result.match_percentage < 100.0 { - all_100_percent = false; - } - } else { - all_100_percent = false; - } - - total_instructions += result.vm_instructions; - } - - println!("\n{}", "=".repeat(50)); - println!("Total VM instructions traced: {}", total_instructions); - - // Check if all required binaries were found and matched - let binaries_found = comparison_results.iter().filter(|r| r.elf_found).count(); - let total_test_cases = comparison_results.len(); - - // Define cleanup function - let cleanup = || { - if let Err(e) = fs::remove_file(vm_log_path) { - // Silently ignore if file doesn't exist or can't be removed - // Only print if there's an unexpected error - if e.kind() != std::io::ErrorKind::NotFound { - println!("Note: Could not remove temporary log file: {}", e); - } - } - }; - - if comparison_results.is_empty() { - cleanup(); - return Err("No test cases found in VM log".to_string()); - } - - if binaries_found == 0 { - println!( - "⚠️ Warning: No ELF binaries found for any of the {} test cases", - total_test_cases - ); - println!( - " To build binaries, run: cargo build -p examples --release --target crates/compiler/targets/avm32.json --features binaries" - ); - println!(" Skipping binary comparison validation."); - println!("\n✅ Test completed (skipped binary validation)"); - cleanup(); - return Ok(()); - } - - if !all_100_percent { - cleanup(); - return Err(format!( - "Not all binaries achieved 100% match. Found {}/{} binaries, not all matched perfectly", - binaries_found, total_test_cases - )); - } - - println!( - "🎉 All {} found binaries matched 100% with VM execution!", - binaries_found - ); - println!("\n✅ Binary comparison test completed successfully!"); - - cleanup(); - Ok(()) -} - -#[derive(Debug)] -struct TestCase { - name: String, - address_mappings: Vec<(String, String)>, - instructions: Vec, -} - -#[derive(Debug)] -#[allow(dead_code)] -struct Instruction { - pc: u32, - bytes: Vec, - mnemonic: String, -} - -#[derive(Debug)] -struct ComparisonResult { - test_name: String, - binary_name: String, - vm_instructions: usize, - elf_found: bool, - match_percentage: f64, -} - -fn extract_test_cases(log_content: &str) -> Vec { - let mut test_cases = Vec::new(); - let mut current_test: Option = None; - let mut in_test = false; - - for line in log_content.lines() { - // Detect test case start - if line.contains("#### Running test case:") { - // Save previous test if exists - if let Some(test) = current_test.take() { - test_cases.push(test); - } - - // Extract test name - let name = line - .split("#### Running test case:") - .nth(1) - .unwrap_or("") - .trim() - .trim_end_matches("####") - .trim() - .to_string(); - - current_test = Some(TestCase { - name, - address_mappings: Vec::new(), - instructions: Vec::new(), - }); - in_test = true; - } - - // Extract address mappings - if in_test && line.contains("->") && !line.contains("Binary Mappings:") { - if let Some(test) = current_test.as_mut() { - let parts: Vec<&str> = line.split("->").collect(); - if parts.len() == 2 { - let address = parts[0].trim().to_string(); - let binary = parts[1].trim().to_string(); - test.address_mappings.push((address, binary)); - } - } - } - - // Extract instructions - if line.starts_with("PC = ") { - if let Some(test) = current_test.as_mut() { - if let Some(instr) = parse_instruction_line(line) { - test.instructions.push(instr); - } - } - } - - // Detect test case end - if line.contains("Execution terminated") { - in_test = false; - } - } - - // Save last test if exists - if let Some(test) = current_test { - test_cases.push(test); - } - - test_cases -} - -fn parse_instruction_line(line: &str) -> Option { - // Parse lines like: PC = 0x00000400, Bytes = [13 01 01 fe], Instr = addi x2, x2, -32 - - let pc_part = line.split(", Bytes").next()?; - let pc_str = pc_part.strip_prefix("PC = 0x")?; - let pc = u32::from_str_radix(pc_str, 16).ok()?; - - let bytes_part = line.split("Bytes = [").nth(1)?; - let bytes_str = bytes_part.split(']').next()?; - let bytes: Vec = bytes_str - .split_whitespace() - .filter_map(|b| u8::from_str_radix(b, 16).ok()) - .collect(); - - let instr_part = line.split("Instr = ").nth(1)?; - let mnemonic = instr_part.to_string(); - - Some(Instruction { - pc, - bytes, - mnemonic, - }) -} - -fn calculate_match_percentage(instructions: &[Instruction]) -> f64 { - // In a real implementation, this would compare with actual ELF instructions - // For this test, we'll simulate that only binaries with sufficient instructions match - if instructions.is_empty() { - 0.0 - } else if instructions.len() < 100 { - // Small instruction count suggests incomplete execution - 50.0 - } else { - // Assume good match for substantial executions - 100.0 - } -} diff --git a/crates/examples/tests/common/ecdsa.rs b/crates/examples/tests/common/ecdsa.rs deleted file mode 100644 index 18d38f2..0000000 --- a/crates/examples/tests/common/ecdsa.rs +++ /dev/null @@ -1,40 +0,0 @@ -use k256::ecdsa::{Signature, SigningKey, signature::hazmat::PrehashSigner}; - -// Fixed test private key (random-looking, non-trivial scalar) -pub const ECDSA_SK_BYTES: [u8; 32] = [ - 0x79, 0x6d, 0x89, 0x3e, 0x8f, 0x16, 0x29, 0x5a, 0xda, 0xfe, 0x04, 0x8c, 0x53, 0x2f, 0xf9, 0x7e, - 0x47, 0x22, 0x92, 0x1a, 0x86, 0xd2, 0xb4, 0x52, 0x38, 0xa1, 0x6c, 0x9e, 0x1b, 0x45, 0xd3, 0x7c, -]; -pub const ECDSA_HASH: [u8; 32] = [ - 0x3b, 0xbd, 0x38, 0x9e, 0x94, 0x1c, 0x63, 0x7f, 0x36, 0x32, 0xaa, 0xf4, 0x2f, 0x93, 0xb7, 0xb1, - 0xf1, 0x7c, 0x6f, 0x31, 0x86, 0x92, 0x01, 0x34, 0x1d, 0x5f, 0x28, 0x40, 0x61, 0x5c, 0xac, 0x2b, -]; - -// Compressed SEC1 encoding of the corresponding public key -pub const ECDSA_PK_BYTES: [u8; 33] = [ - 0x02, 0xda, 0x8c, 0x8e, 0x0a, 0x4e, 0x5d, 0xfc, 0x76, 0x6f, 0xf1, 0xcb, 0xda, 0x27, 0x03, 0xea, - 0xcd, 0xb0, 0xdf, 0x07, 0xda, 0x19, 0xde, 0x65, 0x03, 0x51, 0x46, 0xdb, 0x9b, 0x9c, 0x8a, 0xb7, - 0x0c, -]; - -// Deterministic signature over ECDSA_HASH with ECDSA_SK_BYTES (r || s), big-endian. -pub const ECDSA_SIG_BYTES: [u8; 64] = [ - 0x13, 0xe3, 0x22, 0xb9, 0x33, 0x19, 0x17, 0x76, 0x6d, 0x8c, 0xbf, 0xe9, 0x9f, 0x1d, 0x44, 0xd8, - 0xeb, 0x4f, 0x1d, 0xb3, 0xca, 0xd1, 0x31, 0xaf, 0x92, 0xb2, 0xf2, 0x26, 0x3c, 0xe6, 0x60, 0x92, - 0x2a, 0x3a, 0xef, 0x94, 0xe6, 0x3e, 0x74, 0x06, 0xf4, 0x20, 0xee, 0x0c, 0x0c, 0xb6, 0x5f, 0xce, - 0xe0, 0x45, 0x26, 0xba, 0x9e, 0x36, 0xf6, 0x20, 0x92, 0x77, 0x73, 0x9d, 0x2d, 0x64, 0x37, 0xa2, -]; - -pub fn build_ecdsa_payload() -> Vec { - // Deterministic key/signature for testing - let pk = ECDSA_PK_BYTES; - let sig = ECDSA_SIG_BYTES; - let hash = ECDSA_HASH; - - let mut payload = Vec::with_capacity(1 + pk.len() + sig.len() + hash.len()); - payload.push(pk.len() as u8); - payload.extend_from_slice(&pk); - payload.extend_from_slice(&sig); - payload.extend_from_slice(&hash); - payload -} diff --git a/crates/examples/tests/common/router.rs b/crates/examples/tests/common/router.rs deleted file mode 100644 index 6ee35d3..0000000 --- a/crates/examples/tests/common/router.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Minimal router helpers used by the host-side tests to encode calls. - -/// Represents a function call input for the VM router. -pub struct HostFuncCall { - pub selector: u8, - pub args: Vec, -} - -/// Encodes multiple function calls into a single buffer for the guest VM router. -pub fn encode_router_calls(calls: &[HostFuncCall]) -> Vec { - let mut encoded = Vec::new(); - - for call in calls { - let len = call.args.len(); - assert!(len <= 255, "argument too long for 1-byte length field"); - - encoded.push(call.selector); - encoded.push(len as u8); - encoded.extend_from_slice(&call.args); - } - - encoded -} diff --git a/crates/examples/tests/common/state.rs b/crates/examples/tests/common/state.rs deleted file mode 100644 index c69d8a7..0000000 --- a/crates/examples/tests/common/state.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::utils::to_address; -use state::State; - -/// Build a test state with prefunded accounts. -pub fn test_state() -> State { - let mut state = State::new(); - - // Prefund the addresses used in examples tests. - for addr_hex in [ - "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2", - "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3", - ] { - let addr = to_address(addr_hex); - let account = state.get_account_mut(&addr); - account.balance = 1_000_000_000u128; // 1 billion am for testing - } - - state -} diff --git a/crates/examples/tests/common/test_runner.rs b/crates/examples/tests/common/test_runner.rs deleted file mode 100644 index cbda489..0000000 --- a/crates/examples/tests/common/test_runner.rs +++ /dev/null @@ -1,299 +0,0 @@ -#![allow(dead_code)] - -use std::env; -use std::fs::{self, File}; -use std::io::Write as IoWrite; -use std::path::{Path, PathBuf}; -use std::rc::Rc; - -use core::cell::RefCell; -use core::fmt::Write; -use bootloader::bootloader::Bootloader; -use state::State; -use crate::state_helper::test_state; - -// File writer for logging to disk -struct FileWriter { - file: File, -} - -impl FileWriter { - fn new(path: &str) -> std::io::Result { - Ok(FileWriter { - file: File::create(path)?, - }) - } -} - -impl Write for FileWriter { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - self.file - .write_all(s.as_bytes()) - .map_err(|_| core::fmt::Error)?; - self.file.flush().map_err(|_| core::fmt::Error)?; - Ok(()) - } -} - -/// Console writer that wraps println! -struct ConsoleWriter; - -impl Write for ConsoleWriter { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - print!("{}", s); - Ok(()) - } -} - -/// Test runner that encapsulates test execution with configurable output -pub struct TestRunner { - writer: Rc>, - verbose: bool, - vm_memory_size: usize, - kernel_bytes: Option>, - kernel_path: Option, -} - -impl TestRunner { - /// Create a new test runner with console output (default) - pub fn new() -> Self { - Self::with_writer(Rc::new(RefCell::new(ConsoleWriter))) - } - - /// Set VM memory size - pub fn with_memory_size(mut self, size: usize) -> Self { - self.vm_memory_size = size; - self - } - - /// Enable or disable verbose mode - pub fn with_verbose(mut self, verbose: bool) -> Self { - self.verbose = verbose; - self - } - - /// Create a test runner with file output - pub fn with_file>(path: P) -> std::io::Result { - let file_writer = FileWriter::new(path.as_ref().to_str().unwrap())?; - Ok(Self::with_writer(Rc::new(RefCell::new(file_writer)))) - } - - /// Create a test runner with a custom writer - pub fn with_writer(writer: Rc>) -> Self { - TestRunner { - writer, - verbose: false, - vm_memory_size: 16 * 1024 * 1024, // larger default to accommodate bigger binaries without RVC - kernel_bytes: Self::load_kernel_from_env(), - kernel_path: env::var("KERNEL_ELF").ok(), - } - } - - /// Execute all test cases - pub fn execute(&self) -> Result<(), String> { - use super::TEST_CASES; - - writeln!(self.writer.borrow_mut(), "=== Starting Test Run ===").unwrap(); - writeln!( - self.writer.borrow_mut(), - "Verbose logging: {}", - if self.verbose { "enabled" } else { "disabled" } - ) - .unwrap(); - - for case in TEST_CASES.iter() { - self.run_test_case(case)?; - } - - // Write test summary - writeln!(self.writer.borrow_mut(), "\n=== Test Run Complete ===").unwrap(); - writeln!( - self.writer.borrow_mut(), - "Total test cases: {}", - TEST_CASES.len() - ) - .unwrap(); - - Ok(()) - } - - /// Run a single test case - fn run_test_case(&self, case: &super::TestCase) -> Result<(), String> { - let mut bootloader = Bootloader::new(self.vm_memory_size); - let state = Rc::new(RefCell::new(test_state())); - - // Write test case header - writeln!( - self.writer.borrow_mut(), - "\n############################################" - ) - .unwrap(); - writeln!( - self.writer.borrow_mut(), - "#### Running test case: {} ####", - case.name - ) - .unwrap(); - writeln!( - self.writer.borrow_mut(), - "############################################" - ) - .unwrap(); - - // Print address to binary mappings - if !case.address_mappings.is_empty() { - writeln!(self.writer.borrow_mut(), "\n📍 Address -> Binary Mappings:").unwrap(); - for (addr, binary) in &case.address_mappings { - writeln!(self.writer.borrow_mut(), " {} -> {}", addr, binary).unwrap(); - } - } - writeln!(self.writer.borrow_mut()).unwrap(); - - // Execute the whole bundle via the bootloader/kernel path. - let result = bootloader.execute_bundle( - self.kernel_bytes.as_ref().ok_or_else(|| { - "KERNEL_ELF not set or unreadable; bootloader path required".to_string() - })?, - &case.bundle, - state, - self.verbose, - if self.verbose { - Some(self.writer.clone()) - } else { - None - }, - ); - - let result = match result { - Some(result) => result, - None => { - return Err("Bootloader returned no receipts".to_string()); - } - }; - if let Some(post_state) = result.state { - println!("\n=== State After Execution ==="); - print_state(&post_state); - } - - let receipt = result - .receipts - .last() - .ok_or_else(|| "No receipts returned from kernel".to_string())?; - writeln!(self.writer.borrow_mut(), "\n=== Receipt ===").unwrap(); - writeln!(self.writer.borrow_mut(), "{receipt}").unwrap(); - let result = receipt.result; - if result.success != case.expected_success { - return Err(format!( - "Expected success={}, got {}", - case.expected_success, result.success - )); - } - let error_code = result.error_code; - if error_code != case.expected_error_code { - return Err(format!( - "Expected error_code={}, got {}", - case.expected_error_code, error_code - )); - } - match &case.expected_data { - Some(expected) => { - let expected_len = expected.len(); - let data_len = result.data_len as usize; - let actual_len = data_len; - if actual_len != expected_len { - return Err(format!( - "Expected data_len={}, got {}", - expected_len, actual_len - )); - } - let actual = &result.data[..actual_len.min(result.data.len())]; - if actual != expected.as_slice() { - return Err(format!( - "Expected data {:?}, got {:?}", - expected, actual - )); - } - } - None => { - let data_len = result.data_len; - if data_len != 0 { - return Err(format!( - "Expected empty data, got data_len={}", - data_len - )); - } - } - } - - writeln!( - self.writer.borrow_mut(), - "✅ Test case '{}' passed", - case.name - ) - .unwrap(); - - // For now we treat successful bootloader execution as a passed test. - Ok(()) - } -} - -fn print_state(state: &State) { - println!("--- State Dump ---"); - for (addr, acc) in &state.accounts { - println!(" Address: 0x{}", hex_encode(addr.0)); - println!(" - Balance: {}", acc.balance); - println!(" - Nonce: {}", acc.nonce); - println!(" - Is contract?: {}", acc.is_contract); - println!(" - Code size: {} bytes", acc.code.len()); - println!(" - Storage:"); - for (key, value) in &acc.storage { - let value_hex = hex_join(value); - println!( - " Key: {:<20} | Value ({} bytes): {}", - key, - value.len(), - value_hex - ); - } - println!(); - } - println!("--------------------"); -} - -fn hex_encode(bytes: [u8; 20]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(40); - for b in bytes { - out.push(HEX[(b >> 4) as usize] as char); - out.push(HEX[(b & 0x0f) as usize] as char); - } - out -} - -fn hex_join(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut out = String::with_capacity(bytes.len().saturating_mul(3)); - for (idx, b) in bytes.iter().enumerate() { - if idx > 0 { - out.push(' '); - } - out.push(HEX[(b >> 4) as usize] as char); - out.push(HEX[(b & 0x0f) as usize] as char); - } - out -} - -impl Default for TestRunner { - fn default() -> Self { - Self::with_writer(Rc::new(RefCell::new(ConsoleWriter))) - } -} - -impl TestRunner { - fn load_kernel_from_env() -> Option> { - let path = env::var("KERNEL_ELF") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../bootloader/bin/kernel.elf")); - fs::read(&path).ok() - } -} diff --git a/crates/examples/tests/common/utils.rs b/crates/examples/tests/common/utils.rs deleted file mode 100644 index 10ae255..0000000 --- a/crates/examples/tests/common/utils.rs +++ /dev/null @@ -1,147 +0,0 @@ -use compiler::elf::parse_elf_from_bytes; -use compiler::{EventAbi, EventParam, ParamType}; -use serde_json::Value; -use std::fs; -use std::path::Path; -use types::address::Address; - -pub fn to_address(hex: &str) -> Address { - assert!(hex.len() == 40, "Hex string must be exactly 40 characters"); - - fn from_hex_char(c: u8) -> u8 { - match c { - b'0'..=b'9' => c - b'0', - b'a'..=b'f' => c - b'a' + 10, - b'A'..=b'F' => c - b'A' + 10, - _ => panic!("Invalid hex character"), - } - } - - let mut bytes = [0u8; 20]; - let hex_bytes = hex.as_bytes(); - for i in 0..20 { - let hi = from_hex_char(hex_bytes[i * 2]); - let lo = from_hex_char(hex_bytes[i * 2 + 1]); - bytes[i] = (hi << 4) | lo; - } - - Address(bytes) -} - -pub fn load_abi_from_file>(path: P) -> Option> { - let content = fs::read_to_string(&path).unwrap_or_else(|_| { - panic!( - "❌ Failed to read ABI file from {}", - path.as_ref().display() - ) - }); - - let json: Value = serde_json::from_str(&content).unwrap_or_else(|_| { - panic!( - "❌ Failed to parse ABI JSON from {}", - path.as_ref().display() - ) - }); - - let events = json.get("events")?; - let events_array = events.as_array()?; - - let mut event_abis = Vec::new(); - for event in events_array { - let name = event.get("name")?.as_str()?.to_string(); - let inputs = event.get("inputs")?.as_array()?; - - let mut params = Vec::new(); - for input in inputs { - let param_name = input.get("name")?.as_str()?.to_string(); - let param_type_str = input.get("type")?.as_str()?; - let indexed = input - .get("indexed") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let param_type = match param_type_str { - "address" => ParamType::Address, - "uint32" => ParamType::Uint(32), - "uint64" => ParamType::Uint(64), - "uint128" => ParamType::Uint(128), - "uint256" => ParamType::Uint(256), - "bool" => ParamType::Bool, - "string" => ParamType::String, - "bytes" => ParamType::Bytes, - _ => panic!("❌ Unsupported parameter type: {}", param_type_str), - }; - - params.push(EventParam { - name: param_name, - kind: param_type, - indexed, - }); - } - - event_abis.push(EventAbi { - name, - inputs: params, - }); - } - - Some(event_abis) -} - -/// Load and merge multiple ABI files (events only). Returns None if no events found. -pub fn load_abis_from_files(paths: &[&str]) -> Option> { - let mut merged = Vec::new(); - for path in paths { - if let Some(mut events) = load_abi_from_file(path) { - merged.append(&mut events); - } - } - if merged.is_empty() { - None - } else { - Some(merged) - } -} - -pub fn get_program_code(name: &str) -> Vec { - // Build the full path - let bin_path = format!("bin/{}", name); - - // Try reading from bin directory first (for compiled binaries) - let bytes = fs::read(&bin_path) - .or_else(|_| { - // Fallback to target directory for development - let target_path = format!("../../target/avm32/release/{}", name); - fs::read(&target_path) - }) - .unwrap_or_else(|_| panic!("❌ Failed to read ELF file: {}", name)); - - let elf = parse_elf_from_bytes(&bytes) - .unwrap_or_else(|_| panic!("❌ Failed to parse ELF from {}", name)); - - let (code, code_start) = elf - .get_flat_code() - .unwrap_or_else(|| panic!("❌ No code sections found in ELF {}", name)); - - let (rodata, rodata_start) = elf - .get_flat_rodata() - .unwrap_or_else(|| (vec![], usize::MAX as u64)); - - let mut total_len = code_start + code.len() as u64; // assumes rodata is after code - if rodata.len() > 0 { - total_len = rodata_start + rodata.len() as u64; // assumes rodata is after code - } - - // Initialize memory with 0x00 - let mut combined = vec![0u8; total_len as usize]; - - // Copy code - combined[code_start as usize..code_start as usize + code.len()].copy_from_slice(&code); - - // Copy rodata (if it exists) - if rodata.len() > 0 { - combined[rodata_start as usize..rodata_start as usize + rodata.len()] - .copy_from_slice(&rodata); - } - combined -} diff --git a/crates/examples/tests/ecdsa_payload_test.rs b/crates/examples/tests/ecdsa_payload_test.rs deleted file mode 100644 index d9982ed..0000000 --- a/crates/examples/tests/ecdsa_payload_test.rs +++ /dev/null @@ -1,39 +0,0 @@ -use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::hazmat::PrehashVerifier}; - -#[path = "common/ecdsa.rs"] -mod ecdsa; - -#[test] -fn test_ecdsa_payload_is_valid() { - // Build payload using the shared helper. - let payload = ecdsa::build_ecdsa_payload(); - - let pk_len = payload[0] as usize; - let pubkey = &payload[1..1 + pk_len]; - let sig = &payload[1 + pk_len..1 + pk_len + 64]; - let hash = &payload[1 + pk_len + 64..]; - - // Ensure the public key matches the hardcoded secret key we use for signing. - let signing_key = - SigningKey::from_bytes(&ecdsa::ECDSA_SK_BYTES.into()).expect("valid sk bytes"); - let expected_vk = signing_key.verifying_key(); - let payload_vk = VerifyingKey::from_sec1_bytes(pubkey).expect("valid pubkey in payload"); - assert_eq!( - expected_vk.to_encoded_point(true), - payload_vk.to_encoded_point(true), - "payload pubkey should match the signing key" - ); - - // Verify the signature against the known hash. - let sig = Signature::from_slice(sig).expect("valid signature bytes"); - payload_vk - .verify_prehash(hash, &sig) - .expect("signature should verify for payload hash"); - - // Double-check we used the expected hash constant. - assert_eq!( - hash, - &ecdsa::ECDSA_HASH, - "payload hash should match the test constant" - ); -} diff --git a/crates/examples/tests/examples_test.rs b/crates/examples/tests/examples_test.rs deleted file mode 100644 index 1c3f886..0000000 --- a/crates/examples/tests/examples_test.rs +++ /dev/null @@ -1,541 +0,0 @@ -#[path = "common/utils.rs"] -mod utils; - -#[path = "common/test_runner.rs"] -mod test_runner; - -#[path = "common/state.rs"] -mod state_helper; - -#[path = "common/ecdsa.rs"] -mod ecdsa; - -#[path = "common/router.rs"] -mod router; - -use router::{HostFuncCall, encode_router_calls}; -use types::transaction::{Transaction, TransactionBundle, TransactionType}; -use compiler::EventAbi; -pub use ecdsa::{ECDSA_HASH, ECDSA_SK_BYTES, build_ecdsa_payload}; -use once_cell::sync::Lazy; -pub use test_runner::TestRunner; -use utils::{get_program_code, load_abi_from_file, load_abis_from_files, to_address}; - -/// Centralized ELF binary paths for testing -pub struct ElfBinary { - pub name: &'static str, - pub path: &'static str, - pub description: &'static str, -} - -/// All ELF binaries used in tests -pub const ELF_BINARIES: &[ElfBinary] = &[ - ElfBinary { - name: "simple", - path: "bin/simple", - description: "Simple test program", - }, - ElfBinary { - name: "multi_func", - path: "bin/multi_func", - description: "Multiple function test", - }, - ElfBinary { - name: "logging", - path: "bin/logging", - description: "Logging functionality test", - }, - ElfBinary { - name: "storage", - path: "bin/storage", - description: "Storage operations test", - }, - ElfBinary { - name: "call_program", - path: "bin/call_program", - description: "Program calling test", - }, - ElfBinary { - name: "erc20", - path: "bin/erc20", - description: "ERC20 token contract", - }, - ElfBinary { - name: "lib_import", - path: "bin/lib_import", - description: "Library import test", - }, - ElfBinary { - name: "allocator_demo", - path: "bin/allocator_demo", - description: "Memory allocator demonstration", - }, - ElfBinary { - name: "native_transfer", - path: "bin/native_transfer", - description: "Native token transfer via syscall", - }, - ElfBinary { - name: "dex", - path: "bin/dex", - description: "Simple AMM between AM and ERC20", - }, -]; - -/// Get an ELF binary by name -pub fn get_elf_by_name(name: &str) -> Option<&'static ElfBinary> { - ELF_BINARIES.iter().find(|elf| elf.name == name) -} - -/// Get the full path for an ELF binary -pub fn get_elf_path(name: &str) -> Option { - get_elf_by_name(name).map(|elf| format!("crates/examples/{}", elf.path)) -} - -#[derive(Debug)] -pub struct TestCase<'a> { - pub name: &'a str, - pub expected_success: bool, - pub expected_error_code: u32, - pub expected_data: Option>, - pub bundle: TransactionBundle, - pub abi: Option>, - pub address_mappings: Vec<(&'a str, &'a str)>, // (address, binary_name) -} - -pub static TEST_CASES: Lazy>> = Lazy::new(|| { - vec![ - TestCase { - name: "erc20", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![128, 240, 250, 2]), // Expected data: 50,000,000 in little-endian - abi: load_abi_from_file("bin/erc20.abi.json"), - address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "erc20")], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - data: get_program_code("erc20"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[HostFuncCall { - selector: 0x01, // initialize - args: (|| { - // max supply - let max_supply: u32 = 100000000; // 100 million - let mut max_supply_bytes: Vec = max_supply.to_le_bytes().to_vec(); - - // decimals - let decimals: u8 = 18; - - // combine - max_supply_bytes.extend(vec![decimals]); - max_supply_bytes - })(), - }]), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[HostFuncCall { - selector: 0x02, // transfer - args: (|| { - // to address (20 bytes) - let to_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"); - let mut args = to_addr.0.to_vec(); - - // amount (4 bytes) - let amount: u32 = 50000000; // 50 million tokens - args.extend(amount.to_le_bytes()); - - args - })(), - }]), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[HostFuncCall { - selector: 0x05, // balance_of - args: (|| { - // check balance of the original caller (d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0) - let owner_addr = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"); - owner_addr.0.to_vec() - })(), - }]), - value: 0, - nonce: 0, - }, - ]), - }, - TestCase { - name: "call program", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian - abi: None, - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "call_program"), - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "simple"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("call_program"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::CreateAccount, - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - data: get_program_code("simple"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: (|| { - let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1") - .0 - .to_vec(); - data.extend(vec![100, 0, 0, 0, 42, 0, 0, 0]); - data - })(), - value: 0, - nonce: 0, - }, - ]), - }, - TestCase { - name: "account create (storage)", - expected_success: true, - expected_error_code: 0, - expected_data: None, - abi: None, - address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "storage")], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("storage"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: vec![], - value: 0, - nonce: 0, - }, - ]), - }, - TestCase { - name: "account create (simple)", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian - abi: None, - address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "simple")], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("simple"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: vec![ - 100, 0, 0, 0, // first u64 = 100 - 42, 0, 0, 0, // second u64 = 42 - ], - value: 0, - nonce: 0, - }, - ]), - }, - TestCase { - name: "multi function (simple)", - expected_success: true, - expected_error_code: 0, - expected_data: Some(vec![100, 0, 0, 0]), // Expected data: 100 in little-endian - abi: None, - address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "multi_func")], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("multi_func"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: encode_router_calls(&[HostFuncCall { - selector: 0x01, - args: vec![ - 100, 0, 0, 0, // first = 100 - 42, 0, 0, 0, // second = 42 - ], - }]), - value: 0, - nonce: 0, - }, - ]), - }, - TestCase { - name: "allocator demo", - expected_success: true, - expected_error_code: 0, - expected_data: None, //Some(b"VM allocator demo completed successfully!".to_vec()), - abi: None, - address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "allocator_demo")], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("allocator_demo"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - // 6 x u32 little-endian: - // Vec: 12, 15, 100; Map: 95, 87, 92 - data: vec![ - 12, 0, 0, 0, 15, 0, 0, 0, 100, 0, 0, 0, 95, 0, 0, 0, 87, 0, 0, 0, 92, 0, 0, - 0, - ], - value: 0, - nonce: 0, - }, - ]), - }, - TestCase { - name: "native transfer", - expected_success: true, - expected_error_code: 0, - expected_data: None, - abi: None, - address_mappings: vec![], - bundle: TransactionBundle::new(vec![Transaction { - tx_type: TransactionType::Transfer, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: vec![], - value: 10, - nonce: 0, - }]), - }, - TestCase { - name: "guest transfer syscall", - expected_success: true, - expected_error_code: 0, - expected_data: Some({ - let mut v = 42u128.to_le_bytes().to_vec(); - v - }), - abi: None, - address_mappings: vec![( - "d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4", - "native_transfer", - )], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: get_program_code("native_transfer"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d4"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: (|| { - let mut data = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0") - .0 - .to_vec(); - data.extend_from_slice(&42u64.to_le_bytes()); - data - })(), - value: 0, - nonce: 1, - }, - ]), - }, - TestCase { - name: "dex amm", - expected_success: true, - expected_error_code: 0, - expected_data: Some({ - let mut buf = Vec::new(); - buf.extend_from_slice(&101000u128.to_le_bytes()); - buf.extend_from_slice(&495050u128.to_le_bytes()); - buf - }), - abi: load_abis_from_files(&["bin/erc20.abi.json", "bin/dex.abi.json"]), - address_mappings: vec![ - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1", "erc20"), - ("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5", "dex"), - ], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: get_program_code("erc20"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: encode_router_calls(&[HostFuncCall { - selector: 0x01, // init - args: (|| { - let mut args = Vec::new(); - let supply: u32 = 1_000_000; - args.extend_from_slice(&supply.to_le_bytes()); - args.push(0); // decimals - args - })(), - }]), - value: 0, - nonce: 1, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d1"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: encode_router_calls(&[HostFuncCall { - selector: 0x03, // approve - args: (|| { - let mut args = to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5") - .0 - .to_vec(); - let amount: u32 = 500_000; - args.extend_from_slice(&amount.to_le_bytes()); - args - })(), - }]), - value: 0, - nonce: 2, - }, - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: get_program_code("dex"), - value: 0, - nonce: 3, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: { - let mut data = Vec::new(); - data.push(0x01); // add liquidity - data.extend_from_slice(&100_000u64.to_le_bytes()); // AM in - data.extend_from_slice(&500_000u64.to_le_bytes()); // token target - data - }, - value: 0, - nonce: 4, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d2"), - data: { - let mut data = Vec::new(); - data.push(0x03); // swap - data.push(0x00); // am -> token - data.extend_from_slice(&1_000u64.to_le_bytes()); - data - }, - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d5"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d3"), - data: { - let mut data = Vec::new(); - data.push(0x02); // remove liquidity - data.extend_from_slice(&100_000u64.to_le_bytes()); - data - }, - value: 0, - nonce: 5, - }, - ]), - }, - TestCase { - name: "ecdsa verify", - expected_success: true, - expected_error_code: 0, - expected_data: None, - abi: None, - address_mappings: vec![("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0", "ecdsa_verify")], - bundle: TransactionBundle::new(vec![ - Transaction { - tx_type: TransactionType::CreateAccount, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: get_program_code("ecdsa_verify"), - value: 0, - nonce: 0, - }, - Transaction { - tx_type: TransactionType::ProgramCall, - to: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - from: to_address("d5a3c7f85d2b6e91fa78cd3210b45f6ae913d0d0"), - data: build_ecdsa_payload(), - value: 0, - nonce: 1, - }, - ]), - }, - ] -}); - -#[test] -fn test_examples() { - TestRunner::default().execute().unwrap() -} diff --git a/crates/kernel/src/tests/results.rs b/crates/kernel/src/tests/results.rs index 9dd4821..c013666 100644 --- a/crates/kernel/src/tests/results.rs +++ b/crates/kernel/src/tests/results.rs @@ -5,8 +5,6 @@ pub struct TestResults { pub detail: u32, } -pub const TEST_RESULTS_ADDR: u32 = 0x0003_f000; - impl TestResults { pub const fn pass(detail: u32) -> Self { Self { @@ -24,7 +22,7 @@ impl TestResults { } pub unsafe fn write_results(results: TestResults) { - let ptr = TEST_RESULTS_ADDR as *mut TestResults; + let ptr = kernel::global::KERNEL_RESULT_ADDR as *mut TestResults; unsafe { ptr.write_volatile(results); } From aac10294c3de9c252ad1f6f582e61cbf99ad2de9 Mon Sep 17 00:00:00 2001 From: Alon Muroch Date: Sun, 4 Jan 2026 19:23:05 +0200 Subject: [PATCH 70/70] make all passes --- Makefile | 12 +----------- crates/compiler/tests/abi_generator_tests.rs | 2 -- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/Makefile b/Makefile index ff87ed7..f26a250 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ KERNEL_OUT_DIR := crates/bootloader/bin KERNEL_BINS := $(shell awk '/\[\[bin\]\]/{inbin=1;next} inbin && /name =/{gsub(/"/,"",$$3); print $$3; inbin=0}' $(KERNEL_MANIFEST)) KERNEL_TEST_BINS := $(filter-out kernel,$(KERNEL_BINS)) -all: clean program examples test utils summary +all: clean examples test utils summary .PHONY: run_examples .PHONY: kernel @@ -35,12 +35,6 @@ clean: @cd utils/binary_comparison && $(MAKE) clean > /dev/null 2>&1 || true @echo "=== Clean complete ===" -program: - @echo "=== Building program ===" - cargo clean -p program - cargo build -p program --target riscv32im-unknown-none-elf - @echo "=== Program build complete ===" - examples: @echo "=== Building example programs ===" $(MAKE) -C crates/examples @@ -49,9 +43,7 @@ examples: test: generate_abis @echo "=== Running tests ===" cargo test -p types -p storage -p state -- --nocapture - cargo test -p program -- --nocapture cargo test -p vm -- --nocapture - cargo test -p os -- --nocapture cargo test -p compiler -- --nocapture cd crates/examples && cargo test -- --nocapture @echo "=== Tests complete ===" @@ -72,7 +64,6 @@ summary: @echo "🎉 BUILD SUMMARY" @echo "================" @echo "✅ Cleaned project artifacts" - @echo "✅ Built program crate for RISC-V target" @echo "✅ Built example programs:" @echo " - allocator_demo: Memory allocation demonstration" @echo " - call_program: Cross-contract call demonstration" @@ -90,7 +81,6 @@ summary: @echo " - types" @echo " - storage" @echo " - state" - @echo " - program" @echo " - vm" @echo " - compiler" @echo "✅ Tested VM instruction soundness:" diff --git a/crates/compiler/tests/abi_generator_tests.rs b/crates/compiler/tests/abi_generator_tests.rs index 7b7761d..97affae 100644 --- a/crates/compiler/tests/abi_generator_tests.rs +++ b/crates/compiler/tests/abi_generator_tests.rs @@ -76,8 +76,6 @@ fn test_function_extraction() { let function_names: Vec<&str> = abi.functions.iter().map(|f| f.name.as_str()).collect(); assert!(function_names.contains(&"init")); assert!(function_names.contains(&"transfer")); - assert!(function_names.contains(&"approve")); - assert!(function_names.contains(&"transfer_from")); assert!(function_names.contains(&"balance_of")); // Check selectors