diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index d3eff1b6..7c7f945b 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -84,6 +84,42 @@ impl VirtualMachineRuntime { fn int_arg_to_u16_status(value: i64) -> Result { u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid) } + + fn is_game_profile_syscall(syscall: Syscall) -> bool { + matches!( + syscall, + Syscall::GfxClear + | Syscall::GfxFillRect + | Syscall::GfxDrawLine + | Syscall::GfxDrawCircle + | Syscall::GfxDrawDisc + | Syscall::GfxDrawSquare + | Syscall::GfxDrawText + | Syscall::GfxClear565 + | Syscall::ComposerBindScene + | Syscall::ComposerUnbindScene + | Syscall::ComposerSetCamera + | Syscall::ComposerEmitSprite + | Syscall::AssetLoad + | Syscall::AssetStatus + | Syscall::AssetCommit + | Syscall::AssetCancel + | Syscall::BankInfo + ) + } + + fn ensure_game_profile_syscall(&self, syscall: Syscall) -> Result<(), VmFault> { + if Self::is_game_profile_syscall(syscall) + && self.current_cartridge_app_mode != AppMode::Game + { + return Err(VmFault::Trap( + TRAP_INVALID_SYSCALL, + format!("{} is only available to the Game profile", syscall.name()), + )); + } + + Ok(()) + } } impl NativeInterface for VirtualMachineRuntime { @@ -108,6 +144,8 @@ impl NativeInterface for VirtualMachineRuntime { _ => {} } + self.ensure_game_profile_syscall(syscall)?; + let hw = ctx.require_hw()?; match syscall { diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index e8152934..30c5eff1 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -64,12 +64,16 @@ impl FsBackend for MemFsBackend { } } -fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { +fn cartridge_with_program_and_mode( + program: Vec, + capabilities: u64, + app_mode: AppMode, +) -> Cartridge { Cartridge { app_id: 42, title: "Test Cart".into(), app_version: "1.0.0".into(), - app_mode: AppMode::Game, + app_mode, capabilities, program, assets: AssetsPayloadSource::empty(), @@ -78,6 +82,10 @@ fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { } } +fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { + cartridge_with_program_and_mode(program, capabilities, AppMode::Game) +} + fn serialized_single_function_module(code: Vec, syscalls: Vec) -> Vec { serialized_single_function_module_with_consts(code, vec![], syscalls) } @@ -260,6 +268,130 @@ fn tick_returns_error_when_vm_ends_slice_with_trap() { assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); } +#[test] +fn tick_system_profile_rejects_gfx_game_surface() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::System); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware).expect("system gfx call must trap"); + + match report { + CrashReport::VmTrap { trap } => { + assert_eq!(trap.code, prometeu_bytecode::TRAP_INVALID_SYSCALL); + assert!(trap.message.contains("GfxClear is only available to the Game profile")); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + } +} + +#[test] +fn tick_system_profile_rejects_composer_game_surface() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "composer".into(), + name: "bind_scene".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program_and_mode(program, caps::GFX, AppMode::System); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = + runtime.tick(&mut vm, &signals, &mut hardware).expect("system composer call must trap"); + + match report { + CrashReport::VmTrap { trap } => { + assert_eq!(trap.code, prometeu_bytecode::TRAP_INVALID_SYSCALL); + assert!( + trap.message.contains("ComposerBindScene is only available to the Game profile") + ); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + } +} + +#[test] +fn tick_system_profile_rejects_bank_game_surface() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "bank".into(), + name: "info".into(), + version: 1, + arg_slots: 1, + ret_slots: 2, + }], + ); + let cartridge = cartridge_with_program_and_mode(program, caps::BANK, AppMode::System); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = + runtime.tick(&mut vm, &signals, &mut hardware).expect("system bank call must trap"); + + match report { + CrashReport::VmTrap { trap } => { + assert_eq!(trap.code, prometeu_bytecode::TRAP_INVALID_SYSCALL); + assert!(trap.message.contains("BankInfo is only available to the Game profile")); + } + other => panic!("expected VmTrap crash report, got {:?}", other), + } +} + +#[test] +fn tick_system_profile_can_use_shared_log_transport() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut vm = VirtualMachine::default(); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + let code = assemble("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 0\nHALT").expect("assemble"); + let program = serialized_single_function_module_with_consts( + code, + vec![ConstantPoolEntry::String("system log".into())], + vec![SyscallDecl { + module: "log".into(), + name: "write".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }], + ); + let cartridge = cartridge_with_program_and_mode(program, caps::LOG, AppMode::System); + + runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick(&mut vm, &signals, &mut hardware); + + assert!(report.is_none(), "system log transport must remain internally shared"); + assert!(vm.is_halted()); +} + #[test] fn tick_returns_panic_report_distinct_from_trap() { let mut runtime = VirtualMachineRuntime::new(None); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 3d255de3..42567bb1 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -29,4 +29,4 @@ {"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"discussion/lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} {"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"discussion/lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-28","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0030-internal-viewport-270p/LSN-0039-resolution-baseline-changes-are-runtime-wide-contracts.md","status":"done","created_at":"2026-04-28","updated_at":"2026-04-28"}]} -{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[{"id":"AGD-0031","file":"AGD-0031-runtime-mode-separation-game-system.md","status":"accepted","created_at":"2026-05-11","updated_at":"2026-05-14"}],"decisions":[{"id":"DEC-0023","file":"DEC-0023-system-pipeline-separation.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14"}],"plans":[{"id":"PLN-0046","file":"PLN-0046-profile-separation-specification.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0047","file":"PLN-0047-firmware-runtime-profile-dispatch.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0048","file":"PLN-0048-minimal-system-hub-pipeline.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0049","file":"PLN-0049-system-profile-abi-gates.md","status":"open","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0050","file":"PLN-0050-system-pipeline-evidence-and-learning.md","status":"open","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[{"id":"AGD-0031","file":"AGD-0031-runtime-mode-separation-game-system.md","status":"accepted","created_at":"2026-05-11","updated_at":"2026-05-14"}],"decisions":[{"id":"DEC-0023","file":"DEC-0023-system-pipeline-separation.md","status":"accepted","created_at":"2026-05-14","updated_at":"2026-05-14"}],"plans":[{"id":"PLN-0046","file":"PLN-0046-profile-separation-specification.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0047","file":"PLN-0047-firmware-runtime-profile-dispatch.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0048","file":"PLN-0048-minimal-system-hub-pipeline.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0049","file":"PLN-0049-system-profile-abi-gates.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]},{"id":"PLN-0050","file":"PLN-0050-system-pipeline-evidence-and-learning.md","status":"open","created_at":"2026-05-14","updated_at":"2026-05-14","ref_decisions":["DEC-0023"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0049-system-profile-abi-gates.md b/discussion/workflow/plans/PLN-0049-system-profile-abi-gates.md index 0723b0c8..abef109e 100644 --- a/discussion/workflow/plans/PLN-0049-system-profile-abi-gates.md +++ b/discussion/workflow/plans/PLN-0049-system-profile-abi-gates.md @@ -2,7 +2,7 @@ id: PLN-0049 ticket: runtime-mode-separation-game-system title: System Profile ABI Gates -status: open +status: done created: 2026-05-14 completed: ref_decisions: [DEC-0023]