implements PR-026
This commit is contained in:
parent
67e7acf73b
commit
0ff4909079
@ -181,7 +181,6 @@ mod tests {
|
||||
title: "Broken Cart".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::Game,
|
||||
entrypoint: "".into(),
|
||||
capabilities: 0,
|
||||
program: vec![0, 0, 0, 0],
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
@ -218,7 +217,6 @@ mod tests {
|
||||
title: "Trap Cart".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::Game,
|
||||
entrypoint: "".into(),
|
||||
capabilities: caps::GFX,
|
||||
program,
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
|
||||
@ -22,7 +22,6 @@ pub struct Cartridge {
|
||||
pub title: String,
|
||||
pub app_version: String,
|
||||
pub app_mode: AppMode,
|
||||
pub entrypoint: String,
|
||||
pub capabilities: CapFlags,
|
||||
pub program: Vec<u8>,
|
||||
pub assets: AssetsPayloadSource,
|
||||
@ -36,7 +35,6 @@ pub struct CartridgeDTO {
|
||||
pub title: String,
|
||||
pub app_version: String,
|
||||
pub app_mode: AppMode,
|
||||
pub entrypoint: String,
|
||||
pub capabilities: CapFlags,
|
||||
pub program: Vec<u8>,
|
||||
pub assets: AssetsPayloadSource,
|
||||
@ -51,7 +49,6 @@ impl From<CartridgeDTO> for Cartridge {
|
||||
title: dto.title,
|
||||
app_version: dto.app_version,
|
||||
app_mode: dto.app_mode,
|
||||
entrypoint: dto.entrypoint,
|
||||
capabilities: dto.capabilities,
|
||||
program: dto.program,
|
||||
assets: dto.assets,
|
||||
@ -305,7 +302,6 @@ pub struct CartridgeManifest {
|
||||
pub title: String,
|
||||
pub app_version: String,
|
||||
pub app_mode: AppMode,
|
||||
pub entrypoint: String,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
@ -85,7 +85,6 @@ impl DirectoryCartridgeLoader {
|
||||
title: manifest.title,
|
||||
app_version: manifest.app_version,
|
||||
app_mode: manifest.app_mode,
|
||||
entrypoint: manifest.entrypoint,
|
||||
capabilities,
|
||||
program,
|
||||
assets,
|
||||
@ -277,8 +276,7 @@ mod tests {
|
||||
"app_id": 1001,
|
||||
"title": "Example",
|
||||
"app_version": "1.0.0",
|
||||
"app_mode": "Game",
|
||||
"entrypoint": "main"
|
||||
"app_mode": "Game"
|
||||
});
|
||||
|
||||
if let Some(capabilities) = capabilities {
|
||||
|
||||
@ -30,7 +30,6 @@ pub struct VirtualMachineRuntime {
|
||||
pub current_cartridge_title: String,
|
||||
pub current_cartridge_app_version: String,
|
||||
pub current_cartridge_app_mode: AppMode,
|
||||
pub current_entrypoint: String,
|
||||
pub logs_written_this_frame: HashMap<u32, u32>,
|
||||
pub telemetry_current: TelemetryFrame,
|
||||
pub telemetry_last: TelemetryFrame,
|
||||
|
||||
@ -23,7 +23,6 @@ impl VirtualMachineRuntime {
|
||||
current_cartridge_title: String::new(),
|
||||
current_cartridge_app_version: String::new(),
|
||||
current_cartridge_app_mode: AppMode::Game,
|
||||
current_entrypoint: String::new(),
|
||||
logs_written_this_frame: HashMap::new(),
|
||||
telemetry_current: TelemetryFrame::default(),
|
||||
telemetry_last: TelemetryFrame::default(),
|
||||
@ -97,7 +96,6 @@ impl VirtualMachineRuntime {
|
||||
self.current_cartridge_title.clear();
|
||||
self.current_cartridge_app_version.clear();
|
||||
self.current_cartridge_app_mode = AppMode::Game;
|
||||
self.current_entrypoint.clear();
|
||||
self.logs_written_this_frame.clear();
|
||||
|
||||
self.telemetry_current = TelemetryFrame::default();
|
||||
@ -122,13 +120,12 @@ impl VirtualMachineRuntime {
|
||||
self.clear_cartridge_state();
|
||||
vm.set_capabilities(cartridge.capabilities);
|
||||
|
||||
match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) {
|
||||
match vm.initialize(cartridge.program.clone()) {
|
||||
Ok(_) => {
|
||||
self.current_app_id = cartridge.app_id;
|
||||
self.current_cartridge_title = cartridge.title.clone();
|
||||
self.current_cartridge_app_version = cartridge.app_version.clone();
|
||||
self.current_cartridge_app_mode = cartridge.app_mode;
|
||||
self.current_entrypoint = cartridge.entrypoint.clone();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@ -59,7 +59,6 @@ fn cartridge_with_program(program: Vec<u8>, capabilities: u64) -> Cartridge {
|
||||
title: "Test Cart".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::Game,
|
||||
entrypoint: "".into(),
|
||||
capabilities,
|
||||
program,
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
@ -270,7 +269,6 @@ fn reset_clears_cartridge_scoped_runtime_state() {
|
||||
runtime.current_cartridge_title = "Cart".into();
|
||||
runtime.current_cartridge_app_version = "1.2.3".into();
|
||||
runtime.current_cartridge_app_mode = AppMode::System;
|
||||
runtime.current_entrypoint = "main".into();
|
||||
runtime.logs_written_this_frame.insert(42, 3);
|
||||
runtime.telemetry_current.frame_index = 8;
|
||||
runtime.telemetry_current.cycles_used = 99;
|
||||
@ -296,7 +294,6 @@ fn reset_clears_cartridge_scoped_runtime_state() {
|
||||
assert!(runtime.current_cartridge_title.is_empty());
|
||||
assert!(runtime.current_cartridge_app_version.is_empty());
|
||||
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
|
||||
assert!(runtime.current_entrypoint.is_empty());
|
||||
assert!(runtime.logs_written_this_frame.is_empty());
|
||||
assert_eq!(runtime.telemetry_current.frame_index, 0);
|
||||
assert_eq!(runtime.telemetry_current.cycles_used, 0);
|
||||
@ -352,7 +349,6 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
|
||||
assert!(runtime.current_cartridge_title.is_empty());
|
||||
assert!(runtime.current_cartridge_app_version.is_empty());
|
||||
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
|
||||
assert!(runtime.current_entrypoint.is_empty());
|
||||
assert!(runtime.open_files.is_empty());
|
||||
assert_eq!(runtime.next_handle, 1);
|
||||
assert!(!runtime.paused);
|
||||
@ -1030,7 +1026,6 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
|
||||
title: "Memcard Game".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::Game,
|
||||
entrypoint: "".into(),
|
||||
capabilities: caps::FS,
|
||||
program,
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
@ -1070,7 +1065,6 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
|
||||
title: "System App".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::System,
|
||||
entrypoint: "".into(),
|
||||
capabilities: caps::FS,
|
||||
program,
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
|
||||
@ -58,7 +58,7 @@ impl VirtualMachineRuntime {
|
||||
self.begin_logical_frame(signals, hw);
|
||||
|
||||
if self.needs_prepare_entry_call || vm.call_stack_is_empty() {
|
||||
vm.prepare_call(&self.current_entrypoint);
|
||||
vm.prepare_boot_call();
|
||||
self.needs_prepare_entry_call = false;
|
||||
}
|
||||
|
||||
|
||||
@ -2598,7 +2598,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_loader_hardening_invalid_magic() {
|
||||
let mut vm = VirtualMachine::default();
|
||||
let res = vm.initialize(vec![0, 0, 0, 0], "");
|
||||
let res = vm.initialize(vec![0, 0, 0, 0]);
|
||||
assert_eq!(res, Err(VmInitError::InvalidFormat));
|
||||
// VM should remain empty
|
||||
assert_eq!(vm.program.rom.len(), 0);
|
||||
@ -2611,7 +2611,7 @@ mod tests {
|
||||
header[0..4].copy_from_slice(b"PBS\0");
|
||||
header[4..6].copy_from_slice(&1u16.to_le_bytes()); // version 1 (unsupported)
|
||||
|
||||
let res = vm.initialize(header, "");
|
||||
let res = vm.initialize(header);
|
||||
assert_eq!(res, Err(VmInitError::UnsupportedFormat));
|
||||
}
|
||||
|
||||
@ -2622,7 +2622,7 @@ mod tests {
|
||||
header[0..4].copy_from_slice(b"PBS\0");
|
||||
header[8..12].copy_from_slice(&1u32.to_le_bytes()); // 1 section claimed but none provided
|
||||
|
||||
let res = vm.initialize(header, "");
|
||||
let res = vm.initialize(header);
|
||||
match res {
|
||||
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {}
|
||||
_ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res),
|
||||
@ -2630,7 +2630,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loader_hardening_entrypoint_not_found() {
|
||||
fn test_loader_hardening_boot_protocol_entrypoint_not_found() {
|
||||
let mut vm = VirtualMachine::default();
|
||||
let header = prometeu_bytecode::model::BytecodeModule {
|
||||
version: 0,
|
||||
@ -2643,8 +2643,7 @@ mod tests {
|
||||
}
|
||||
.serialize();
|
||||
|
||||
// Try to initialize with numeric entrypoint 10 (out of bounds for empty ROM)
|
||||
let res = vm.initialize(header, "10");
|
||||
let res = vm.initialize(header);
|
||||
assert_eq!(res, Err(VmInitError::EntrypointNotFound));
|
||||
|
||||
// VM state should not be updated
|
||||
@ -2656,22 +2655,27 @@ mod tests {
|
||||
fn test_loader_hardening_successful_init() {
|
||||
let mut vm = VirtualMachine::default();
|
||||
vm.pc = 123; // Pollution
|
||||
let code = assemble("HALT").expect("assemble");
|
||||
|
||||
let header = prometeu_bytecode::model::BytecodeModule {
|
||||
version: 0,
|
||||
const_pool: vec![],
|
||||
functions: vec![],
|
||||
code: vec![],
|
||||
functions: vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: code.len() as u32,
|
||||
..Default::default()
|
||||
}],
|
||||
code,
|
||||
debug_info: None,
|
||||
exports: vec![],
|
||||
syscalls: vec![],
|
||||
}
|
||||
.serialize();
|
||||
|
||||
let res = vm.initialize(header, "");
|
||||
let res = vm.initialize(header);
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(vm.pc, 0);
|
||||
assert_eq!(vm.program.rom.len(), 0);
|
||||
assert_eq!(vm.program.rom.len(), 2);
|
||||
assert_eq!(vm.cycles, 0);
|
||||
}
|
||||
|
||||
@ -2692,7 +2696,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert!(res.is_ok(), "patched hostcall should initialize");
|
||||
|
||||
@ -2718,7 +2722,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -2758,7 +2762,7 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -2790,7 +2794,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -2820,7 +2824,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -2853,7 +2857,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -2888,7 +2892,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -2959,7 +2963,7 @@ mod tests {
|
||||
let mut vm = VirtualMachine::default();
|
||||
vm.set_capabilities(prometeu_hal::syscalls::caps::ALL);
|
||||
let bytes = serialized_single_hostcall_module(syscall);
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
assert!(res.is_ok(), "status-first signature must be accepted: {:?}", res);
|
||||
}
|
||||
}
|
||||
@ -3015,7 +3019,7 @@ mod tests {
|
||||
let mut vm = VirtualMachine::default();
|
||||
vm.set_capabilities(prometeu_hal::syscalls::caps::ALL);
|
||||
let bytes = serialized_single_hostcall_module(syscall.clone());
|
||||
let err = vm.initialize(bytes, "").expect_err("legacy ABI must be rejected");
|
||||
let err = vm.initialize(bytes).expect_err("legacy ABI must be rejected");
|
||||
match err {
|
||||
VmInitError::LoaderPatchFailed(crate::vm_init_error::LoaderPatchError::ResolveFailed(
|
||||
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
|
||||
@ -3048,7 +3052,7 @@ mod tests {
|
||||
let mut header = vec![0u8; 32];
|
||||
header[0..4].copy_from_slice(b"PBS\0");
|
||||
|
||||
let res = vm.initialize(header, "");
|
||||
let res = vm.initialize(header);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
@ -3080,7 +3084,7 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
let res = vm.initialize(bytes, "");
|
||||
let res = vm.initialize(bytes);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
|
||||
@ -68,11 +68,7 @@ fn patch_module_hostcalls(
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
pub fn initialize(
|
||||
&mut self,
|
||||
program_bytes: Vec<u8>,
|
||||
entrypoint: &str,
|
||||
) -> Result<(), VmInitError> {
|
||||
pub fn initialize(&mut self, program_bytes: Vec<u8>) -> Result<(), VmInitError> {
|
||||
self.program = ProgramImage::default();
|
||||
self.pc = 0;
|
||||
self.operand_stack.clear();
|
||||
@ -113,23 +109,11 @@ impl VirtualMachine {
|
||||
return Err(VmInitError::InvalidFormat);
|
||||
};
|
||||
|
||||
let pc = if entrypoint.is_empty() {
|
||||
program.functions.first().map(|f| f.code_offset as usize).unwrap_or(0)
|
||||
} else if let Ok(func_idx) = entrypoint.parse::<usize>() {
|
||||
program
|
||||
.functions
|
||||
.get(func_idx)
|
||||
.map(|f| f.code_offset as usize)
|
||||
.ok_or(VmInitError::EntrypointNotFound)?
|
||||
} else if let Some(&func_idx) = program.exports.get(entrypoint) {
|
||||
program
|
||||
.functions
|
||||
.get(func_idx as usize)
|
||||
.map(|f| f.code_offset as usize)
|
||||
.ok_or(VmInitError::EntrypointNotFound)?
|
||||
} else {
|
||||
return Err(VmInitError::EntrypointNotFound);
|
||||
};
|
||||
let pc = program
|
||||
.functions
|
||||
.first()
|
||||
.map(|f| f.code_offset as usize)
|
||||
.ok_or(VmInitError::EntrypointNotFound)?;
|
||||
|
||||
self.program = program;
|
||||
self.pc = pc;
|
||||
@ -142,6 +126,10 @@ impl VirtualMachine {
|
||||
self.capabilities = caps;
|
||||
}
|
||||
|
||||
pub fn prepare_boot_call(&mut self) {
|
||||
self.prepare_call_by_index(0);
|
||||
}
|
||||
|
||||
pub fn prepare_call(&mut self, entrypoint: &str) {
|
||||
let func_idx = if let Ok(idx) = entrypoint.parse::<usize>() {
|
||||
idx
|
||||
@ -149,6 +137,10 @@ impl VirtualMachine {
|
||||
self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0)
|
||||
};
|
||||
|
||||
self.prepare_call_by_index(func_idx);
|
||||
}
|
||||
|
||||
fn prepare_call_by_index(&mut self, func_idx: usize) {
|
||||
let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default();
|
||||
self.pc = callee.code_offset as usize;
|
||||
self.halted = false;
|
||||
|
||||
@ -5,6 +5,6 @@ fn invalid_image_format_is_rejected_before_execution() {
|
||||
// Provide bytes that are not a valid PBS image. The VM must reject it with InvalidFormat.
|
||||
let program_bytes = b"NOT_PBS_IMAGE".to_vec();
|
||||
let mut vm = VirtualMachine::default();
|
||||
let result = vm.initialize(program_bytes, "0");
|
||||
let result = vm.initialize(program_bytes);
|
||||
assert!(matches!(result, Err(VmInitError::InvalidFormat)));
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ pub fn generate() -> Result<()> {
|
||||
if assets_pa_path.exists() {
|
||||
fs::remove_file(&assets_pa_path)?;
|
||||
}
|
||||
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"entrypoint\": \"main\",\n \"capabilities\": [\"gfx\", \"log\"]\n}\n")?;
|
||||
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\"]\n}\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user