implements PR-026

This commit is contained in:
bQUARKz 2026-03-22 23:55:07 +00:00
parent 67e7acf73b
commit 0ff4909079
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 44 additions and 66 deletions

View File

@ -181,7 +181,6 @@ mod tests {
title: "Broken Cart".into(), title: "Broken Cart".into(),
app_version: "1.0.0".into(), app_version: "1.0.0".into(),
app_mode: AppMode::Game, app_mode: AppMode::Game,
entrypoint: "".into(),
capabilities: 0, capabilities: 0,
program: vec![0, 0, 0, 0], program: vec![0, 0, 0, 0],
assets: AssetsPayloadSource::empty(), assets: AssetsPayloadSource::empty(),
@ -218,7 +217,6 @@ mod tests {
title: "Trap Cart".into(), title: "Trap Cart".into(),
app_version: "1.0.0".into(), app_version: "1.0.0".into(),
app_mode: AppMode::Game, app_mode: AppMode::Game,
entrypoint: "".into(),
capabilities: caps::GFX, capabilities: caps::GFX,
program, program,
assets: AssetsPayloadSource::empty(), assets: AssetsPayloadSource::empty(),

View File

@ -22,7 +22,6 @@ pub struct Cartridge {
pub title: String, pub title: String,
pub app_version: String, pub app_version: String,
pub app_mode: AppMode, pub app_mode: AppMode,
pub entrypoint: String,
pub capabilities: CapFlags, pub capabilities: CapFlags,
pub program: Vec<u8>, pub program: Vec<u8>,
pub assets: AssetsPayloadSource, pub assets: AssetsPayloadSource,
@ -36,7 +35,6 @@ pub struct CartridgeDTO {
pub title: String, pub title: String,
pub app_version: String, pub app_version: String,
pub app_mode: AppMode, pub app_mode: AppMode,
pub entrypoint: String,
pub capabilities: CapFlags, pub capabilities: CapFlags,
pub program: Vec<u8>, pub program: Vec<u8>,
pub assets: AssetsPayloadSource, pub assets: AssetsPayloadSource,
@ -51,7 +49,6 @@ impl From<CartridgeDTO> for Cartridge {
title: dto.title, title: dto.title,
app_version: dto.app_version, app_version: dto.app_version,
app_mode: dto.app_mode, app_mode: dto.app_mode,
entrypoint: dto.entrypoint,
capabilities: dto.capabilities, capabilities: dto.capabilities,
program: dto.program, program: dto.program,
assets: dto.assets, assets: dto.assets,
@ -305,7 +302,6 @@ pub struct CartridgeManifest {
pub title: String, pub title: String,
pub app_version: String, pub app_version: String,
pub app_mode: AppMode, pub app_mode: AppMode,
pub entrypoint: String,
#[serde(default)] #[serde(default)]
pub capabilities: Vec<Capability>, pub capabilities: Vec<Capability>,
} }

View File

@ -85,7 +85,6 @@ impl DirectoryCartridgeLoader {
title: manifest.title, title: manifest.title,
app_version: manifest.app_version, app_version: manifest.app_version,
app_mode: manifest.app_mode, app_mode: manifest.app_mode,
entrypoint: manifest.entrypoint,
capabilities, capabilities,
program, program,
assets, assets,
@ -277,8 +276,7 @@ mod tests {
"app_id": 1001, "app_id": 1001,
"title": "Example", "title": "Example",
"app_version": "1.0.0", "app_version": "1.0.0",
"app_mode": "Game", "app_mode": "Game"
"entrypoint": "main"
}); });
if let Some(capabilities) = capabilities { if let Some(capabilities) = capabilities {

View File

@ -30,7 +30,6 @@ pub struct VirtualMachineRuntime {
pub current_cartridge_title: String, pub current_cartridge_title: String,
pub current_cartridge_app_version: String, pub current_cartridge_app_version: String,
pub current_cartridge_app_mode: AppMode, pub current_cartridge_app_mode: AppMode,
pub current_entrypoint: String,
pub logs_written_this_frame: HashMap<u32, u32>, pub logs_written_this_frame: HashMap<u32, u32>,
pub telemetry_current: TelemetryFrame, pub telemetry_current: TelemetryFrame,
pub telemetry_last: TelemetryFrame, pub telemetry_last: TelemetryFrame,

View File

@ -23,7 +23,6 @@ impl VirtualMachineRuntime {
current_cartridge_title: String::new(), current_cartridge_title: String::new(),
current_cartridge_app_version: String::new(), current_cartridge_app_version: String::new(),
current_cartridge_app_mode: AppMode::Game, current_cartridge_app_mode: AppMode::Game,
current_entrypoint: String::new(),
logs_written_this_frame: HashMap::new(), logs_written_this_frame: HashMap::new(),
telemetry_current: TelemetryFrame::default(), telemetry_current: TelemetryFrame::default(),
telemetry_last: TelemetryFrame::default(), telemetry_last: TelemetryFrame::default(),
@ -97,7 +96,6 @@ impl VirtualMachineRuntime {
self.current_cartridge_title.clear(); self.current_cartridge_title.clear();
self.current_cartridge_app_version.clear(); self.current_cartridge_app_version.clear();
self.current_cartridge_app_mode = AppMode::Game; self.current_cartridge_app_mode = AppMode::Game;
self.current_entrypoint.clear();
self.logs_written_this_frame.clear(); self.logs_written_this_frame.clear();
self.telemetry_current = TelemetryFrame::default(); self.telemetry_current = TelemetryFrame::default();
@ -122,13 +120,12 @@ impl VirtualMachineRuntime {
self.clear_cartridge_state(); self.clear_cartridge_state();
vm.set_capabilities(cartridge.capabilities); vm.set_capabilities(cartridge.capabilities);
match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) { match vm.initialize(cartridge.program.clone()) {
Ok(_) => { Ok(_) => {
self.current_app_id = cartridge.app_id; self.current_app_id = cartridge.app_id;
self.current_cartridge_title = cartridge.title.clone(); self.current_cartridge_title = cartridge.title.clone();
self.current_cartridge_app_version = cartridge.app_version.clone(); self.current_cartridge_app_version = cartridge.app_version.clone();
self.current_cartridge_app_mode = cartridge.app_mode; self.current_cartridge_app_mode = cartridge.app_mode;
self.current_entrypoint = cartridge.entrypoint.clone();
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {

View File

@ -59,7 +59,6 @@ fn cartridge_with_program(program: Vec<u8>, capabilities: u64) -> Cartridge {
title: "Test Cart".into(), title: "Test Cart".into(),
app_version: "1.0.0".into(), app_version: "1.0.0".into(),
app_mode: AppMode::Game, app_mode: AppMode::Game,
entrypoint: "".into(),
capabilities, capabilities,
program, program,
assets: AssetsPayloadSource::empty(), assets: AssetsPayloadSource::empty(),
@ -270,7 +269,6 @@ fn reset_clears_cartridge_scoped_runtime_state() {
runtime.current_cartridge_title = "Cart".into(); runtime.current_cartridge_title = "Cart".into();
runtime.current_cartridge_app_version = "1.2.3".into(); runtime.current_cartridge_app_version = "1.2.3".into();
runtime.current_cartridge_app_mode = AppMode::System; runtime.current_cartridge_app_mode = AppMode::System;
runtime.current_entrypoint = "main".into();
runtime.logs_written_this_frame.insert(42, 3); runtime.logs_written_this_frame.insert(42, 3);
runtime.telemetry_current.frame_index = 8; runtime.telemetry_current.frame_index = 8;
runtime.telemetry_current.cycles_used = 99; 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_title.is_empty());
assert!(runtime.current_cartridge_app_version.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty());
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
assert!(runtime.current_entrypoint.is_empty());
assert!(runtime.logs_written_this_frame.is_empty()); assert!(runtime.logs_written_this_frame.is_empty());
assert_eq!(runtime.telemetry_current.frame_index, 0); assert_eq!(runtime.telemetry_current.frame_index, 0);
assert_eq!(runtime.telemetry_current.cycles_used, 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_title.is_empty());
assert!(runtime.current_cartridge_app_version.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty());
assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game);
assert!(runtime.current_entrypoint.is_empty());
assert!(runtime.open_files.is_empty()); assert!(runtime.open_files.is_empty());
assert_eq!(runtime.next_handle, 1); assert_eq!(runtime.next_handle, 1);
assert!(!runtime.paused); assert!(!runtime.paused);
@ -1030,7 +1026,6 @@ fn tick_memcard_slot_roundtrip_for_game_profile() {
title: "Memcard Game".into(), title: "Memcard Game".into(),
app_version: "1.0.0".into(), app_version: "1.0.0".into(),
app_mode: AppMode::Game, app_mode: AppMode::Game,
entrypoint: "".into(),
capabilities: caps::FS, capabilities: caps::FS,
program, program,
assets: AssetsPayloadSource::empty(), assets: AssetsPayloadSource::empty(),
@ -1070,7 +1065,6 @@ fn tick_memcard_access_is_denied_for_non_game_profile() {
title: "System App".into(), title: "System App".into(),
app_version: "1.0.0".into(), app_version: "1.0.0".into(),
app_mode: AppMode::System, app_mode: AppMode::System,
entrypoint: "".into(),
capabilities: caps::FS, capabilities: caps::FS,
program, program,
assets: AssetsPayloadSource::empty(), assets: AssetsPayloadSource::empty(),

View File

@ -58,7 +58,7 @@ impl VirtualMachineRuntime {
self.begin_logical_frame(signals, hw); self.begin_logical_frame(signals, hw);
if self.needs_prepare_entry_call || vm.call_stack_is_empty() { 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; self.needs_prepare_entry_call = false;
} }

View File

@ -2598,7 +2598,7 @@ mod tests {
#[test] #[test]
fn test_loader_hardening_invalid_magic() { fn test_loader_hardening_invalid_magic() {
let mut vm = VirtualMachine::default(); 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)); assert_eq!(res, Err(VmInitError::InvalidFormat));
// VM should remain empty // VM should remain empty
assert_eq!(vm.program.rom.len(), 0); assert_eq!(vm.program.rom.len(), 0);
@ -2611,7 +2611,7 @@ mod tests {
header[0..4].copy_from_slice(b"PBS\0"); header[0..4].copy_from_slice(b"PBS\0");
header[4..6].copy_from_slice(&1u16.to_le_bytes()); // version 1 (unsupported) 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)); assert_eq!(res, Err(VmInitError::UnsupportedFormat));
} }
@ -2622,7 +2622,7 @@ mod tests {
header[0..4].copy_from_slice(b"PBS\0"); 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 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 { match res {
Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {} Err(VmInitError::ImageLoadFailed(prometeu_bytecode::LoadError::UnexpectedEof)) => {}
_ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res), _ => panic!("Expected PbsV0LoadFailed(UnexpectedEof), got {:?}", res),
@ -2630,7 +2630,7 @@ mod tests {
} }
#[test] #[test]
fn test_loader_hardening_entrypoint_not_found() { fn test_loader_hardening_boot_protocol_entrypoint_not_found() {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let header = prometeu_bytecode::model::BytecodeModule { let header = prometeu_bytecode::model::BytecodeModule {
version: 0, version: 0,
@ -2643,8 +2643,7 @@ mod tests {
} }
.serialize(); .serialize();
// Try to initialize with numeric entrypoint 10 (out of bounds for empty ROM) let res = vm.initialize(header);
let res = vm.initialize(header, "10");
assert_eq!(res, Err(VmInitError::EntrypointNotFound)); assert_eq!(res, Err(VmInitError::EntrypointNotFound));
// VM state should not be updated // VM state should not be updated
@ -2656,22 +2655,27 @@ mod tests {
fn test_loader_hardening_successful_init() { fn test_loader_hardening_successful_init() {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
vm.pc = 123; // Pollution vm.pc = 123; // Pollution
let code = assemble("HALT").expect("assemble");
let header = prometeu_bytecode::model::BytecodeModule { let header = prometeu_bytecode::model::BytecodeModule {
version: 0, version: 0,
const_pool: vec![], const_pool: vec![],
functions: vec![], functions: vec![FunctionMeta {
code: vec![], code_offset: 0,
code_len: code.len() as u32,
..Default::default()
}],
code,
debug_info: None, debug_info: None,
exports: vec![], exports: vec![],
syscalls: vec![], syscalls: vec![],
} }
.serialize(); .serialize();
let res = vm.initialize(header, ""); let res = vm.initialize(header);
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!(vm.pc, 0); assert_eq!(vm.pc, 0);
assert_eq!(vm.program.rom.len(), 0); assert_eq!(vm.program.rom.len(), 2);
assert_eq!(vm.cycles, 0); 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"); 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!( assert_eq!(
res, res,
@ -2758,7 +2762,7 @@ mod tests {
], ],
); );
let res = vm.initialize(bytes, ""); let res = vm.initialize(bytes);
assert_eq!( assert_eq!(
res, res,
@ -2790,7 +2794,7 @@ mod tests {
}], }],
); );
let res = vm.initialize(bytes, ""); let res = vm.initialize(bytes);
assert_eq!( assert_eq!(
res, res,
@ -2820,7 +2824,7 @@ mod tests {
}], }],
); );
let res = vm.initialize(bytes, ""); let res = vm.initialize(bytes);
assert_eq!( assert_eq!(
res, res,
@ -2853,7 +2857,7 @@ mod tests {
}], }],
); );
let res = vm.initialize(bytes, ""); let res = vm.initialize(bytes);
assert_eq!( assert_eq!(
res, res,
@ -2888,7 +2892,7 @@ mod tests {
}], }],
); );
let res = vm.initialize(bytes, ""); let res = vm.initialize(bytes);
assert_eq!( assert_eq!(
res, res,
@ -2959,7 +2963,7 @@ mod tests {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
vm.set_capabilities(prometeu_hal::syscalls::caps::ALL); vm.set_capabilities(prometeu_hal::syscalls::caps::ALL);
let bytes = serialized_single_hostcall_module(syscall); 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); assert!(res.is_ok(), "status-first signature must be accepted: {:?}", res);
} }
} }
@ -3015,7 +3019,7 @@ mod tests {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
vm.set_capabilities(prometeu_hal::syscalls::caps::ALL); vm.set_capabilities(prometeu_hal::syscalls::caps::ALL);
let bytes = serialized_single_hostcall_module(syscall.clone()); 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 { match err {
VmInitError::LoaderPatchFailed(crate::vm_init_error::LoaderPatchError::ResolveFailed( VmInitError::LoaderPatchFailed(crate::vm_init_error::LoaderPatchError::ResolveFailed(
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch { prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
@ -3048,7 +3052,7 @@ mod tests {
let mut header = vec![0u8; 32]; let mut header = vec![0u8; 32];
header[0..4].copy_from_slice(b"PBS\0"); header[0..4].copy_from_slice(b"PBS\0");
let res = vm.initialize(header, ""); let res = vm.initialize(header);
assert_eq!( assert_eq!(
res, res,
@ -3080,7 +3084,7 @@ mod tests {
], ],
); );
let res = vm.initialize(bytes, ""); let res = vm.initialize(bytes);
assert_eq!( assert_eq!(
res, res,

View File

@ -68,11 +68,7 @@ fn patch_module_hostcalls(
} }
impl VirtualMachine { impl VirtualMachine {
pub fn initialize( pub fn initialize(&mut self, program_bytes: Vec<u8>) -> Result<(), VmInitError> {
&mut self,
program_bytes: Vec<u8>,
entrypoint: &str,
) -> Result<(), VmInitError> {
self.program = ProgramImage::default(); self.program = ProgramImage::default();
self.pc = 0; self.pc = 0;
self.operand_stack.clear(); self.operand_stack.clear();
@ -113,23 +109,11 @@ impl VirtualMachine {
return Err(VmInitError::InvalidFormat); return Err(VmInitError::InvalidFormat);
}; };
let pc = if entrypoint.is_empty() { let pc = program
program.functions.first().map(|f| f.code_offset as usize).unwrap_or(0)
} else if let Ok(func_idx) = entrypoint.parse::<usize>() {
program
.functions .functions
.get(func_idx) .first()
.map(|f| f.code_offset as usize) .map(|f| f.code_offset as usize)
.ok_or(VmInitError::EntrypointNotFound)? .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);
};
self.program = program; self.program = program;
self.pc = pc; self.pc = pc;
@ -142,6 +126,10 @@ impl VirtualMachine {
self.capabilities = caps; self.capabilities = caps;
} }
pub fn prepare_boot_call(&mut self) {
self.prepare_call_by_index(0);
}
pub fn prepare_call(&mut self, entrypoint: &str) { pub fn prepare_call(&mut self, entrypoint: &str) {
let func_idx = if let Ok(idx) = entrypoint.parse::<usize>() { let func_idx = if let Ok(idx) = entrypoint.parse::<usize>() {
idx idx
@ -149,6 +137,10 @@ impl VirtualMachine {
self.program.exports.get(entrypoint).map(|&idx| idx as usize).ok_or(()).unwrap_or(0) 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(); let callee = self.program.functions.get(func_idx).cloned().unwrap_or_default();
self.pc = callee.code_offset as usize; self.pc = callee.code_offset as usize;
self.halted = false; self.halted = false;

View File

@ -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. // 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 program_bytes = b"NOT_PBS_IMAGE".to_vec();
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
let result = vm.initialize(program_bytes, "0"); let result = vm.initialize(program_bytes);
assert!(matches!(result, Err(VmInitError::InvalidFormat))); assert!(matches!(result, Err(VmInitError::InvalidFormat)));
} }

View File

@ -93,7 +93,7 @@ pub fn generate() -> Result<()> {
if assets_pa_path.exists() { if assets_pa_path.exists() {
fs::remove_file(&assets_pa_path)?; 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(()) Ok(())
} }