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(),
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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
let pc = program
.functions
.get(func_idx)
.first()
.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);
};
.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;

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

View File

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