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);
}
// 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" => {
if ops.is_empty() {
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_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" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));

View File

@ -13,6 +13,7 @@
//! - `MAKE_CLOSURE fn=<u32>, captures=<u32>`
//! - `SPAWN fn=<u32>, 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.
//!
//! Notes:
@ -58,7 +59,8 @@ fn format_operand(op: CoreOpCode, imm: &[u8]) -> String {
| CoreOpCode::GetLocal
| CoreOpCode::SetLocal
| CoreOpCode::Call
| CoreOpCode::Sleep => {
| CoreOpCode::Sleep
| CoreOpCode::Hostcall => {
let v = u32::from_le_bytes(imm.try_into().unwrap());
format!("{}", v)
}

View File

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

View File

@ -185,6 +185,11 @@ pub enum OpCode {
Sleep = 0x56,
// --- 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).
/// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall)
@ -246,6 +251,7 @@ impl TryFrom<u16> for OpCode {
0x55 => Ok(OpCode::Yield),
0x56 => Ok(OpCode::Sleep),
0x70 => Ok(OpCode::Syscall),
0x71 => Ok(OpCode::Hostcall),
0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
}
@ -304,6 +310,7 @@ impl OpCode {
OpCode::Yield => 1,
OpCode::Sleep => 1,
OpCode::Syscall => 1,
OpCode::Hostcall => 1,
OpCode::FrameSync => 1,
}
}

View File

@ -493,6 +493,16 @@ impl OpCodeSpecExt for OpCode {
// Considered a safepoint since it forces a frame boundary
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 {
name: "SYSCALL",
imm_bytes: 4,

View File

@ -11,7 +11,7 @@ fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
#[test]
fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
// 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();
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);
// SLEEP 3
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)
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC

View File

@ -28,6 +28,8 @@ fn snapshot_representative_program_is_stable() {
emit(CoreOpCode::Yield, None, &mut prog);
// SLEEP 3
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
// HOSTCALL 2
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
// SYSCALL 0x1003
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC
@ -36,6 +38,6 @@ fn snapshot_representative_program_is_stable() {
emit(CoreOpCode::Halt, None, &mut prog);
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);
}

View File

@ -39,7 +39,7 @@ fn disasm(bytes: &[u8]) -> String {
CoreOpCode::PushF64 => format!("{}", instr.imm_f64().unwrap()),
CoreOpCode::PushBool => format!("{}", instr.imm_u8().unwrap()),
CoreOpCode::PushI32 => format!("{}", instr.imm_i32().unwrap()),
CoreOpCode::PopN | CoreOpCode::PushConst => {
CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
format!("{}", instr.imm_u32().unwrap())
}
_ => format!("0x{}", hex::encode(instr.imm)),
@ -115,6 +115,20 @@ fn disasm_contains_expected_mnemonics_and_operands() {
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
mod hex {
pub fn encode(bytes: &[u8]) -> String {

View File

@ -67,6 +67,10 @@ pub enum VerifierError {
pc: usize,
id: u32,
},
HostcallNotPatched {
pc: usize,
sysc_index: u32,
},
/// 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
/// in a terminator.
@ -254,6 +258,12 @@ impl Verifier {
(arg_count, 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 => {
let id = instr.imm_u32().unwrap();
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 }));
}
#[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]
fn test_function_without_terminator_is_rejected() {
// 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();
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 => {
// Immediate carries (fn_id, capture_count)
let (fn_id, cap_count) = instr
@ -2639,9 +2648,16 @@ mod tests {
#[test]
fn test_loader_hardening_entrypoint_not_found() {
let mut vm = VirtualMachine::default();
// Valid empty PBS v0 module
let mut header = vec![0u8; 32];
header[0..4].copy_from_slice(b"PBS\0");
let header = prometeu_bytecode::model::BytecodeModule {
version: 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)
let res = vm.initialize(header, "10");
@ -2657,8 +2673,16 @@ mod tests {
let mut vm = VirtualMachine::default();
vm.pc = 123; // Pollution
let mut header = vec![0u8; 32];
header[0..4].copy_from_slice(b"PBS\0");
let header = prometeu_bytecode::model::BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![],
code: vec![],
debug_info: None,
exports: vec![],
syscalls: vec![],
}
.serialize();
let res = vm.initialize(header, "");
assert!(res.is_ok());