This commit is contained in:
bQUARKz 2026-03-02 14:39:56 +00:00
parent df9b248c98
commit 24e7a3b083
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 242 additions and 4 deletions

View File

@ -15,4 +15,4 @@ mod vm_init_error;
pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId}; pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine}; pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
pub use vm_init_error::VmInitError; pub use vm_init_error::{LoaderPatchError, VmInitError};

View File

@ -4,9 +4,11 @@ use crate::object::ObjectKind;
use crate::roots::{RootVisitor, visit_value_for_roots}; use crate::roots::{RootVisitor, visit_value_for_roots};
use crate::scheduler::Scheduler; use crate::scheduler::Scheduler;
use crate::verifier::Verifier; use crate::verifier::Verifier;
use crate::vm_init_error::VmInitError; use crate::vm_init_error::{LoaderPatchError, VmInitError};
use crate::{HostContext, NativeInterface}; use crate::{HostContext, NativeInterface};
use prometeu_bytecode::HeapRef; use prometeu_bytecode::HeapRef;
use prometeu_bytecode::decode_next;
use prometeu_bytecode::model::BytecodeModule;
use prometeu_bytecode::ProgramImage; use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value; use prometeu_bytecode::Value;
use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
@ -17,6 +19,66 @@ use prometeu_bytecode::{
use prometeu_hal::syscalls::caps::ALL; use prometeu_hal::syscalls::caps::ALL;
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
fn patch_module_hostcalls(
module: &mut BytecodeModule,
capabilities: prometeu_hal::syscalls::CapFlags,
) -> Result<(), LoaderPatchError> {
let resolved = prometeu_hal::syscalls::resolve_declared_program_syscalls(
&module.syscalls,
capabilities,
)
.map_err(LoaderPatchError::ResolveFailed)?;
let mut used = vec![false; module.syscalls.len()];
let mut pc = 0usize;
while pc < module.code.len() {
let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?;
let next_pc = instr.next_pc;
if instr.opcode == OpCode::Hostcall {
let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?;
let Some(resolved_syscall) = resolved.get(sysc_index as usize) else {
return Err(LoaderPatchError::HostcallIndexOutOfBounds {
pc,
sysc_index,
syscalls_len: resolved.len(),
});
};
used[sysc_index as usize] = true;
module.code[pc..pc + 2].copy_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
module.code[pc + 2..pc + 6].copy_from_slice(&resolved_syscall.id.to_le_bytes());
}
pc = next_pc;
}
for (index, (was_used, decl)) in used.iter().zip(&module.syscalls).enumerate() {
if !was_used {
return Err(LoaderPatchError::UnusedSyscallDecl {
sysc_index: index as u32,
module: decl.module.clone(),
name: decl.name.clone(),
version: decl.version,
});
}
}
let mut pc = 0usize;
while pc < module.code.len() {
let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?;
if instr.opcode == OpCode::Hostcall {
return Err(LoaderPatchError::HostcallRemaining {
pc,
sysc_index: instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?,
});
}
pc = instr.next_pc;
}
Ok(())
}
/// Reason why the Virtual Machine stopped execution during a specific run. /// Reason why the Virtual Machine stopped execution during a specific run.
/// This allows the system to decide if it should continue execution in the next tick /// This allows the system to decide if it should continue execution in the next tick
/// or if the frame is finalized. /// or if the frame is finalized.
@ -214,7 +276,10 @@ impl VirtualMachine {
// Only recognized format is loadable: PBS v0 industrial format // Only recognized format is loadable: PBS v0 industrial format
let program = if program_bytes.starts_with(b"PBS\0") { let program = if program_bytes.starts_with(b"PBS\0") {
match prometeu_bytecode::BytecodeLoader::load(&program_bytes) { match prometeu_bytecode::BytecodeLoader::load(&program_bytes) {
Ok(module) => { Ok(mut module) => {
patch_module_hostcalls(&mut module, self.capabilities)
.map_err(VmInitError::LoaderPatchFailed)?;
// Run verifier on the module // Run verifier on the module
let max_stacks = Verifier::verify(&module.code, &module.functions) let max_stacks = Verifier::verify(&module.code, &module.functions)
.map_err(|e| VmInitError::VerificationFailed(format!("{:?}", e)))?; .map_err(|e| VmInitError::VerificationFailed(format!("{:?}", e)))?;
@ -1576,7 +1641,10 @@ mod tests {
vm vm
} }
use crate::HostReturn; use crate::HostReturn;
use prometeu_bytecode::{FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW}; use prometeu_bytecode::model::{BytecodeModule, SyscallDecl};
use prometeu_bytecode::{
FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW, assemble, disassemble,
};
use prometeu_hal::expect_int; use prometeu_hal::expect_int;
struct MockNative; struct MockNative;
@ -1592,6 +1660,23 @@ mod tests {
} }
} }
fn serialized_single_function_module(code: Vec<u8>, syscalls: Vec<SyscallDecl>) -> Vec<u8> {
BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![FunctionMeta {
code_offset: 0,
code_len: code.len() as u32,
..Default::default()
}],
code,
debug_info: None,
exports: vec![],
syscalls,
}
.serialize()
}
#[test] #[test]
fn sleep_delays_execution_by_ticks() { fn sleep_delays_execution_by_ticks() {
let mut native = MockNative; let mut native = MockNative;
@ -2691,6 +2776,137 @@ mod tests {
assert_eq!(vm.cycles, 0); assert_eq!(vm.cycles, 0);
} }
#[test]
fn test_loader_patching_rewrites_hostcall_before_verification() {
let mut vm = VirtualMachine::default();
vm.set_capabilities(prometeu_hal::syscalls::caps::GFX);
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
);
let res = vm.initialize(bytes, "");
assert!(res.is_ok(), "patched hostcall should initialize");
let text = disassemble(&vm.program.rom).expect("disassemble patched rom");
assert!(text.contains("SYSCALL 0x1001"));
assert!(!text.contains("HOSTCALL"));
}
#[test]
fn test_loader_patching_rejects_hostcall_index_out_of_bounds() {
let mut vm = VirtualMachine::default();
vm.set_capabilities(prometeu_hal::syscalls::caps::GFX);
let code = assemble("HOSTCALL 1\nHALT").expect("assemble");
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
);
let res = vm.initialize(bytes, "");
assert_eq!(
res,
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::HostcallIndexOutOfBounds {
pc: 0,
sysc_index: 1,
syscalls_len: 1,
},
))
);
}
#[test]
fn test_loader_patching_rejects_unused_syscall_decl() {
let mut vm = VirtualMachine::default();
vm.set_capabilities(prometeu_hal::syscalls::caps::GFX);
let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble");
let bytes = serialized_single_function_module(
code,
vec![
SyscallDecl {
module: "gfx".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
SyscallDecl {
module: "gfx".into(),
name: "draw_text".into(),
version: 1,
arg_slots: 4,
ret_slots: 0,
},
],
);
let res = vm.initialize(bytes, "");
assert_eq!(
res,
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::UnusedSyscallDecl {
sysc_index: 1,
module: "gfx".into(),
name: "draw_text".into(),
version: 1,
},
))
);
}
#[test]
fn test_loader_patching_propagates_resolution_failure() {
let mut vm = VirtualMachine::default();
vm.set_capabilities(prometeu_hal::syscalls::caps::GFX);
let code = assemble("HOSTCALL 0\nHALT").expect("assemble");
let bytes = serialized_single_function_module(
code,
vec![SyscallDecl {
module: "gfx".into(),
name: "missing".into(),
version: 1,
arg_slots: 0,
ret_slots: 0,
}],
);
let res = vm.initialize(bytes, "");
assert_eq!(
res,
Err(VmInitError::LoaderPatchFailed(
crate::vm_init_error::LoaderPatchError::ResolveFailed(
prometeu_hal::syscalls::DeclaredLoadError::UnknownSyscall {
module: "gfx".into(),
name: "missing".into(),
version: 1,
},
),
))
);
}
#[test] #[test]
fn test_calling_convention_add() { fn test_calling_convention_add() {
let mut native = MockNative; let mut native = MockNative;

View File

@ -1,8 +1,30 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoaderPatchError {
DecodeFailed(prometeu_bytecode::DecodeError),
ResolveFailed(prometeu_hal::syscalls::DeclaredLoadError),
HostcallIndexOutOfBounds {
pc: usize,
sysc_index: u32,
syscalls_len: usize,
},
UnusedSyscallDecl {
sysc_index: u32,
module: String,
name: String,
version: u16,
},
HostcallRemaining {
pc: usize,
sysc_index: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum VmInitError { pub enum VmInitError {
InvalidFormat, InvalidFormat,
UnsupportedFormat, UnsupportedFormat,
ImageLoadFailed(prometeu_bytecode::LoadError), ImageLoadFailed(prometeu_bytecode::LoadError),
LoaderPatchFailed(LoaderPatchError),
EntrypointNotFound, EntrypointNotFound,
VerificationFailed(String), VerificationFailed(String),
} }