This commit is contained in:
bQUARKz 2026-03-02 14:33:03 +00:00
parent 2a79f641dd
commit 96062bf1d0
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
10 changed files with 101 additions and 10 deletions

View File

@ -184,7 +184,7 @@ pub fn assemble(src: &str) -> Result<Vec<u8>, AsmError> {
emit_u16(CoreOpCode::FrameSync as u16, &mut out); emit_u16(CoreOpCode::FrameSync as u16, &mut out);
} }
// One u32 immediate (decimal or hex accepted for SYSCALL only; others decimal ok) // One u32 immediate (decimal or hex accepted; SYSCALL/HOSTCALL commonly use hex/idx)
"JMP" => { "JMP" => {
if ops.is_empty() { if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into())); return Err(AsmError::MissingOperand(line.into()));
@ -338,6 +338,13 @@ pub fn assemble(src: &str) -> Result<Vec<u8>, AsmError> {
emit_u16(CoreOpCode::Sleep as u16, &mut out); emit_u16(CoreOpCode::Sleep as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out); emit_u32(parse_u32_any(ops)?, &mut out);
} }
"HOSTCALL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Hostcall as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"SYSCALL" => { "SYSCALL" => {
if ops.is_empty() { if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into())); return Err(AsmError::MissingOperand(line.into()));

View File

@ -13,6 +13,7 @@
//! - `MAKE_CLOSURE fn=<u32>, captures=<u32>` //! - `MAKE_CLOSURE fn=<u32>, captures=<u32>`
//! - `SPAWN fn=<u32>, argc=<u32>` //! - `SPAWN fn=<u32>, argc=<u32>`
//! - `CALL_CLOSURE argc=<u32>` //! - `CALL_CLOSURE argc=<u32>`
//! - `HOSTCALL <index>` is printed in decimal because it is a `SYSC` table index.
//! - `SYSCALL` is printed as `SYSCALL 0xhhhh` (numeric id in hex) to avoid cross-crate deps. //! - `SYSCALL` is printed as `SYSCALL 0xhhhh` (numeric id in hex) to avoid cross-crate deps.
//! //!
//! Notes: //! Notes:
@ -58,7 +59,8 @@ fn format_operand(op: CoreOpCode, imm: &[u8]) -> String {
| CoreOpCode::GetLocal | CoreOpCode::GetLocal
| CoreOpCode::SetLocal | CoreOpCode::SetLocal
| CoreOpCode::Call | CoreOpCode::Call
| CoreOpCode::Sleep => { | CoreOpCode::Sleep
| CoreOpCode::Hostcall => {
let v = u32::from_le_bytes(imm.try_into().unwrap()); let v = u32::from_le_bytes(imm.try_into().unwrap());
format!("{}", v) format!("{}", v)
} }

View File

@ -679,6 +679,7 @@ fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
| OpCode::GetLocal | OpCode::GetLocal
| OpCode::SetLocal | OpCode::SetLocal
| OpCode::PopN | OpCode::PopN
| OpCode::Hostcall
| OpCode::Syscall => { | OpCode::Syscall => {
pos += 4; pos += 4;
} }

View File

@ -185,6 +185,11 @@ pub enum OpCode {
Sleep = 0x56, Sleep = 0x56,
// --- 6.8 Peripherals and System --- // --- 6.8 Peripherals and System ---
/// Pre-load host binding call by `SYSC` table index.
/// Operand: sysc_index (u32)
/// This opcode is valid only in PBX artifact form and must be patched by the loader
/// into a final numeric `SYSCALL <id>` before verification or execution.
Hostcall = 0x71,
/// Invokes a system function (Firmware/OS). /// Invokes a system function (Firmware/OS).
/// Operand: syscall_id (u32) /// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall) /// Stack: [args...] -> [results...] (depends on syscall)
@ -246,6 +251,7 @@ impl TryFrom<u16> for OpCode {
0x55 => Ok(OpCode::Yield), 0x55 => Ok(OpCode::Yield),
0x56 => Ok(OpCode::Sleep), 0x56 => Ok(OpCode::Sleep),
0x70 => Ok(OpCode::Syscall), 0x70 => Ok(OpCode::Syscall),
0x71 => Ok(OpCode::Hostcall),
0x80 => Ok(OpCode::FrameSync), 0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)), _ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
} }
@ -304,6 +310,7 @@ impl OpCode {
OpCode::Yield => 1, OpCode::Yield => 1,
OpCode::Sleep => 1, OpCode::Sleep => 1,
OpCode::Syscall => 1, OpCode::Syscall => 1,
OpCode::Hostcall => 1,
OpCode::FrameSync => 1, OpCode::FrameSync => 1,
} }
} }

View File

@ -493,6 +493,16 @@ impl OpCodeSpecExt for OpCode {
// Considered a safepoint since it forces a frame boundary // Considered a safepoint since it forces a frame boundary
is_safepoint: true, is_safepoint: true,
}, },
OpCode::Hostcall => OpcodeSpec {
name: "HOSTCALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Syscall => OpcodeSpec { OpCode::Syscall => OpcodeSpec {
name: "SYSCALL", name: "SYSCALL",
imm_bytes: 4, imm_bytes: 4,

View File

@ -11,7 +11,7 @@ fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
#[test] #[test]
fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() { fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
// Program: PUSH_I32 7; MAKE_CLOSURE fn=1,captures=0; CALL_CLOSURE argc=1; // Program: PUSH_I32 7; MAKE_CLOSURE fn=1,captures=0; CALL_CLOSURE argc=1;
// SPAWN fn=2,argc=1; YIELD; SLEEP 3; SYSCALL 0x1003; FRAME_SYNC; HALT // SPAWN fn=2,argc=1; YIELD; SLEEP 3; HOSTCALL 2; SYSCALL 0x1003; FRAME_SYNC; HALT
let mut prog = Vec::new(); let mut prog = Vec::new();
emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog); emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog);
@ -31,6 +31,8 @@ fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
emit(CoreOpCode::Yield, None, &mut prog); emit(CoreOpCode::Yield, None, &mut prog);
// SLEEP 3 // SLEEP 3
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog); emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
// HOSTCALL sysc[2]
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
// SYSCALL gfx.draw_line (0x1003) // SYSCALL gfx.draw_line (0x1003)
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog); emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC // FRAME_SYNC

View File

@ -28,6 +28,8 @@ fn snapshot_representative_program_is_stable() {
emit(CoreOpCode::Yield, None, &mut prog); emit(CoreOpCode::Yield, None, &mut prog);
// SLEEP 3 // SLEEP 3
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog); emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
// HOSTCALL 2
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
// SYSCALL 0x1003 // SYSCALL 0x1003
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog); emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC // FRAME_SYNC
@ -36,6 +38,6 @@ fn snapshot_representative_program_is_stable() {
emit(CoreOpCode::Halt, None, &mut prog); emit(CoreOpCode::Halt, None, &mut prog);
let text = disassemble(&prog).expect("disasm ok"); let text = disassemble(&prog).expect("disasm ok");
let expected = "PUSH_I32 7\nMAKE_CLOSURE fn=1, captures=0\nCALL_CLOSURE argc=1\nSPAWN fn=2, argc=1\nYIELD\nSLEEP 3\nSYSCALL 0x1003\nFRAME_SYNC\nHALT"; let expected = "PUSH_I32 7\nMAKE_CLOSURE fn=1, captures=0\nCALL_CLOSURE argc=1\nSPAWN fn=2, argc=1\nYIELD\nSLEEP 3\nHOSTCALL 2\nSYSCALL 0x1003\nFRAME_SYNC\nHALT";
assert_eq!(text, expected); assert_eq!(text, expected);
} }

View File

@ -39,7 +39,7 @@ fn disasm(bytes: &[u8]) -> String {
CoreOpCode::PushF64 => format!("{}", instr.imm_f64().unwrap()), CoreOpCode::PushF64 => format!("{}", instr.imm_f64().unwrap()),
CoreOpCode::PushBool => format!("{}", instr.imm_u8().unwrap()), CoreOpCode::PushBool => format!("{}", instr.imm_u8().unwrap()),
CoreOpCode::PushI32 => format!("{}", instr.imm_i32().unwrap()), CoreOpCode::PushI32 => format!("{}", instr.imm_i32().unwrap()),
CoreOpCode::PopN | CoreOpCode::PushConst => { CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
format!("{}", instr.imm_u32().unwrap()) format!("{}", instr.imm_u32().unwrap())
} }
_ => format!("0x{}", hex::encode(instr.imm)), _ => format!("0x{}", hex::encode(instr.imm)),
@ -115,6 +115,20 @@ fn disasm_contains_expected_mnemonics_and_operands() {
assert!(text.contains("HALT")); assert!(text.contains("HALT"));
} }
#[test]
fn hostcall_roundtrips_with_decimal_index() {
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Hostcall, Some(&7u32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = disasm(&prog);
assert!(text.contains("HOSTCALL 7"));
let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble hostcall");
assert_eq!(rebuilt, prog);
}
// Minimal hex helper to avoid extra deps in tests // Minimal hex helper to avoid extra deps in tests
mod hex { mod hex {
pub fn encode(bytes: &[u8]) -> String { pub fn encode(bytes: &[u8]) -> String {

View File

@ -67,6 +67,10 @@ pub enum VerifierError {
pc: usize, pc: usize,
id: u32, id: u32,
}, },
HostcallNotPatched {
pc: usize,
sysc_index: u32,
},
/// Execution can fall through past the end of the function without a valid terminator /// Execution can fall through past the end of the function without a valid terminator
/// (e.g., RET, JMP to end, HALT/TRAP). Verifier requires every reachable path to end /// (e.g., RET, JMP to end, HALT/TRAP). Verifier requires every reachable path to end
/// in a terminator. /// in a terminator.
@ -254,6 +258,12 @@ impl Verifier {
(arg_count, 0) (arg_count, 0)
} }
OpCode::Ret => (func.return_slots, 0), OpCode::Ret => (func.return_slots, 0),
OpCode::Hostcall => {
return Err(VerifierError::HostcallNotPatched {
pc: func_start + pc,
sysc_index: instr.imm_u32().unwrap(),
});
}
OpCode::Syscall => { OpCode::Syscall => {
let id = instr.imm_u32().unwrap(); let id = instr.imm_u32().unwrap();
let syscall = Syscall::from_u32(id).ok_or_else(|| { let syscall = Syscall::from_u32(id).ok_or_else(|| {
@ -1145,6 +1155,18 @@ mod tests {
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF })); assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
} }
#[test]
fn test_verifier_rejects_unpatched_hostcall() {
let mut code = Vec::new();
code.push(OpCode::Hostcall as u8);
code.push(0x00);
code.extend_from_slice(&3u32.to_le_bytes());
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::HostcallNotPatched { pc: 0, sysc_index: 3 }));
}
#[test] #[test]
fn test_function_without_terminator_is_rejected() { fn test_function_without_terminator_is_rejected() {
// Single NOP with no RET/JMP/TRAP/HALT at the end → fallthrough to end // Single NOP with no RET/JMP/TRAP/HALT at the end → fallthrough to end

View File

@ -699,6 +699,15 @@ impl VirtualMachine {
self.handle_safepoint(); self.handle_safepoint();
return Err(LogicalFrameEndingReason::FrameSync); return Err(LogicalFrameEndingReason::FrameSync);
} }
OpCode::Hostcall => {
let sysc_index = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))?;
return Err(LogicalFrameEndingReason::Panic(format!(
"HOSTCALL {} reached execution without loader patching",
sysc_index
)));
}
OpCode::MakeClosure => { OpCode::MakeClosure => {
// Immediate carries (fn_id, capture_count) // Immediate carries (fn_id, capture_count)
let (fn_id, cap_count) = instr let (fn_id, cap_count) = instr
@ -2639,9 +2648,16 @@ mod tests {
#[test] #[test]
fn test_loader_hardening_entrypoint_not_found() { fn test_loader_hardening_entrypoint_not_found() {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
// Valid empty PBS v0 module let header = prometeu_bytecode::model::BytecodeModule {
let mut header = vec![0u8; 32]; version: 0,
header[0..4].copy_from_slice(b"PBS\0"); const_pool: vec![],
functions: vec![],
code: vec![],
debug_info: None,
exports: vec![],
syscalls: vec![],
}
.serialize();
// Try to initialize with numeric entrypoint 10 (out of bounds for empty ROM) // Try to initialize with numeric entrypoint 10 (out of bounds for empty ROM)
let res = vm.initialize(header, "10"); let res = vm.initialize(header, "10");
@ -2657,8 +2673,16 @@ mod tests {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();
vm.pc = 123; // Pollution vm.pc = 123; // Pollution
let mut header = vec![0u8; 32]; let header = prometeu_bytecode::model::BytecodeModule {
header[0..4].copy_from_slice(b"PBS\0"); version: 0,
const_pool: vec![],
functions: vec![],
code: vec![],
debug_info: None,
exports: vec![],
syscalls: vec![],
}
.serialize();
let res = vm.initialize(header, ""); let res = vm.initialize(header, "");
assert!(res.is_ok()); assert!(res.is_ok());