PR005: sync ABI verifier/loader and regenerate stress cartridge

This commit is contained in:
bQUARKz 2026-03-09 07:11:03 +00:00
parent c8b3b6afcc
commit 70ffadab6c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 258 additions and 2 deletions

View File

@ -167,3 +167,103 @@ fn declared_resolver_rejects_abi_mismatch() {
} }
); );
} }
#[test]
fn status_first_syscall_signatures_are_pinned() {
let set_sprite = meta_for(Syscall::GfxSetSprite);
assert_eq!(set_sprite.arg_slots, 10);
assert_eq!(set_sprite.ret_slots, 1);
let audio_play_sample = meta_for(Syscall::AudioPlaySample);
assert_eq!(audio_play_sample.arg_slots, 5);
assert_eq!(audio_play_sample.ret_slots, 1);
let audio_play = meta_for(Syscall::AudioPlay);
assert_eq!(audio_play.arg_slots, 7);
assert_eq!(audio_play.ret_slots, 1);
let asset_load = meta_for(Syscall::AssetLoad);
assert_eq!(asset_load.arg_slots, 3);
assert_eq!(asset_load.ret_slots, 2);
let asset_commit = meta_for(Syscall::AssetCommit);
assert_eq!(asset_commit.arg_slots, 1);
assert_eq!(asset_commit.ret_slots, 1);
let asset_cancel = meta_for(Syscall::AssetCancel);
assert_eq!(asset_cancel.arg_slots, 1);
assert_eq!(asset_cancel.ret_slots, 1);
}
#[test]
fn declared_resolver_rejects_legacy_status_first_signatures() {
let declared = vec![
prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
version: 1,
arg_slots: 10,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "audio".into(),
name: "play_sample".into(),
version: 1,
arg_slots: 5,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 3,
ret_slots: 1,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
];
for decl in declared {
let err = resolve_declared_program_syscalls(std::slice::from_ref(&decl), caps::ALL)
.expect_err("legacy signature must be rejected");
match err {
DeclaredLoadError::AbiMismatch {
module,
name,
version,
declared_arg_slots,
declared_ret_slots,
expected_arg_slots,
expected_ret_slots,
} => {
assert_eq!(module, decl.module);
assert_eq!(name, decl.name);
assert_eq!(version, decl.version);
assert_eq!(declared_arg_slots, decl.arg_slots);
assert_eq!(declared_ret_slots, decl.ret_slots);
assert_eq!(expected_arg_slots, declared_arg_slots);
assert_ne!(expected_ret_slots, declared_ret_slots);
}
other => panic!("expected AbiMismatch, got {:?}", other),
}
}
}

View File

@ -1259,6 +1259,16 @@ mod tests {
.serialize() .serialize()
} }
fn serialized_single_hostcall_module(syscall: SyscallDecl) -> Vec<u8> {
let mut source = String::new();
for _ in 0..syscall.arg_slots {
source.push_str("PUSH_I32 0\n");
}
source.push_str("HOSTCALL 0\nHALT");
let code = assemble(&source).expect("assemble");
serialized_single_function_module(code, vec![syscall])
}
#[test] #[test]
fn sleep_delays_execution_by_ticks() { fn sleep_delays_execution_by_ticks() {
let mut native = MockNative; let mut native = MockNative;
@ -2863,6 +2873,140 @@ mod tests {
); );
} }
#[test]
fn test_loader_patching_accepts_status_first_signatures() {
let cases = vec![
SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
version: 1,
arg_slots: 10,
ret_slots: 1,
},
SyscallDecl {
module: "audio".into(),
name: "play_sample".into(),
version: 1,
arg_slots: 5,
ret_slots: 1,
},
SyscallDecl {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 3,
ret_slots: 2,
},
SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
];
for syscall in cases {
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, "");
assert!(res.is_ok(), "status-first signature must be accepted: {:?}", res);
}
}
#[test]
fn test_loader_patching_rejects_legacy_status_first_ret_slots() {
let cases = vec![
SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
version: 1,
arg_slots: 10,
ret_slots: 0,
},
SyscallDecl {
module: "audio".into(),
name: "play_sample".into(),
version: 1,
arg_slots: 5,
ret_slots: 0,
},
SyscallDecl {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 0,
},
SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 3,
ret_slots: 1,
},
SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
];
for syscall in cases {
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");
match err {
VmInitError::LoaderPatchFailed(crate::vm_init_error::LoaderPatchError::ResolveFailed(
prometeu_hal::syscalls::DeclaredLoadError::AbiMismatch {
module,
name,
version,
declared_arg_slots,
declared_ret_slots,
expected_arg_slots,
expected_ret_slots,
},
)) => {
assert_eq!(module, syscall.module);
assert_eq!(name, syscall.name);
assert_eq!(version, syscall.version);
assert_eq!(declared_arg_slots, syscall.arg_slots);
assert_eq!(declared_ret_slots, syscall.ret_slots);
assert_eq!(expected_arg_slots, declared_arg_slots);
assert_ne!(expected_ret_slots, declared_ret_slots);
}
other => panic!("expected ABI mismatch, got {:?}", other),
}
}
}
#[test] #[test]
fn test_loader_hardening_missing_sysc_section() { fn test_loader_hardening_missing_sysc_section() {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();

View File

@ -41,6 +41,13 @@ pub fn generate() -> Result<()> {
arg_slots: 2, arg_slots: 2,
ret_slots: 0, ret_slots: 0,
}, },
SyscallDecl {
module: "gfx".into(),
name: "set_sprite".into(),
version: 1,
arg_slots: 10,
ret_slots: 1,
},
]; ];
heavy_load(&mut rom); heavy_load(&mut rom);
@ -60,6 +67,7 @@ pub fn generate() -> Result<()> {
const_pool: vec![ const_pool: vec![
ConstantPoolEntry::String("stress".into()), ConstantPoolEntry::String("stress".into()),
ConstantPoolEntry::String("frame".into()), ConstantPoolEntry::String("frame".into()),
ConstantPoolEntry::String("missing_tile_bank".into()),
], ],
functions, functions,
code: rom, code: rom,
@ -100,7 +108,7 @@ fn heavy_load(rom: &mut Vec<u8>) {
// Loop: // Loop:
// t = (t + 1) // t = (t + 1)
// clear screen // clear screen
// draw 100 discs using t for animation // draw 500 discs using t for animation
// draw 20 texts using t for animation // draw 20 texts using t for animation
// RET (runtime handles the frame loop) // RET (runtime handles the frame loop)
@ -115,6 +123,10 @@ fn heavy_load(rom: &mut Vec<u8>) {
// --- clear screen --- // --- clear screen ---
rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
// --- call status-first syscall path once per frame and drop status ---
rom.extend(asm(
"PUSH_CONST 2\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1",
));
// --- draw 500 discs --- // --- draw 500 discs ---
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
@ -145,7 +157,7 @@ fn heavy_load(rom: &mut Vec<u8>) {
// --- draw 20 texts --- // --- draw 20 texts ---
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
let text_loop_start = rom.len() as u32; let text_loop_start = rom.len() as u32;
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 0\nLT")); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 20\nLT"));
let jif_text_end_offset = rom.len() + 2; let jif_text_end_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0")); rom.extend(asm("JMP_IF_FALSE 0"));