1use javm_cap::{CacheDirectory, Cap, CapHash, CapHashOrRef, SlotIdx};
23use javm_exec::{Access, CopyingMemory, ExitReason, GasCounter, Interpreter, Mem, Regs};
24
25use crate::callstack::{CallStack, DEFAULT_MAX_DEPTH, Entry, EntryStatus, InstanceEntry};
26use crate::ecall::{CachedEcallHandler, host_op};
27use crate::error::VmError;
28use crate::image_cache::ImageCache;
29use crate::kernel_assist::{KernelAssist, KernelImage, kernel_image_hash};
30
31#[derive(Debug)]
38pub enum CallResult {
39 Halt {
40 return_value: u64,
42 post_instance_hash: CapHash,
45 reflected_slot0: Option<CapHashOrRef>,
48 gas_used: u64,
50 },
51 Faulted {
52 reason: ExitReason,
53 reflected_slot0: Option<CapHashOrRef>,
55 gas_used: u64,
56 },
57 Paused {
58 marker_payload: Option<CapHashOrRef>,
61 gas_used: u64,
62 },
63}
64
65pub struct Vm<K: KernelAssist> {
69 pub stack: CallStack,
70 pub kernel_assist: K,
71 pub image_cache: ImageCache,
72}
73
74impl<K: KernelAssist> Vm<K> {
75 pub fn new(kernel_assist: K) -> Self {
76 Self::with_max_depth(kernel_assist, DEFAULT_MAX_DEPTH)
77 }
78
79 pub fn with_max_depth(kernel_assist: K, max_depth: usize) -> Self {
80 Self {
81 stack: CallStack::new(max_depth),
82 kernel_assist,
83 image_cache: ImageCache::new(),
84 }
85 }
86
87 pub fn invoke_cached(
97 &mut self,
98 cache: &mut CacheDirectory,
99 instance_hash: CapHash,
100 endpoint_idx: u8,
101 args: [u64; 4],
102 gas_budget: u64,
103 ) -> Result<CallResult, VmError> {
104 let (entry, mem, regs, gas, gas_initial) = self.build_entry(
108 cache,
109 CapHashOrRef::Hash(instance_hash),
110 endpoint_idx,
111 args,
112 gas_budget,
113 )?;
114
115 let pushed_pos = self.stack.entries().len();
117 self.stack.push_instance(entry)?;
118 self.drive_and_translate(cache, regs, mem, gas, gas_initial, pushed_pos)
119 }
120
121 pub(crate) fn build_entry(
126 &mut self,
127 cache: &CacheDirectory,
128 inst_ref: CapHashOrRef,
129 endpoint_idx: u8,
130 args: [u64; 4],
131 gas_budget: u64,
132 ) -> Result<(InstanceEntry, Mem, Regs, GasCounter, u64), VmError> {
133 let instance_cap = cache
134 .get(inst_ref.clone())
135 .ok_or(VmError::InstanceNotFound)?;
136 let inst = match &*instance_cap {
137 Cap::Instance(i) => i.clone(),
138 _ => return Err(VmError::InstanceNotFound),
139 };
140 let image_cap = cache
141 .get(CapHashOrRef::Hash(inst.image_hash))
142 .ok_or(VmError::ImageNotFound)?;
143 let img = match &*image_cap {
144 Cap::Image(i) => i.clone(),
145 _ => return Err(VmError::ImageNotFound),
146 };
147
148 let root_cnode_cap = cache
150 .get(inst.root_cnode.clone())
151 .ok_or(VmError::Invariant("instance root_cnode missing in cache"))?;
152 let root_cnode = match &*root_cnode_cap {
153 Cap::CNode(cn) => cn.clone(),
154 _ => {
155 return Err(VmError::Invariant(
156 "root_cnode does not point at Cap::CNode",
157 ));
158 }
159 };
160
161 let unpacked_bitmask = javm_exec::unpack_bitmask(img.bitmask.as_slice(), img.code.len());
163 let program = self.image_cache.get_or_decode(
164 inst.image_hash,
165 img.code.as_slice().to_vec(),
166 unpacked_bitmask,
167 img.jump_table.as_slice().to_vec(),
168 )?;
169
170 let endpoint = img
173 .endpoints
174 .get(endpoint_idx as usize)
175 .ok_or(VmError::Invariant("endpoint index out of range"))?;
176
177 let mut mem = CopyingMemory::new();
180 let mem_size_pages = page_round_up_u64(inst.mem_size as u64);
181 if mem_size_pages > 0 {
182 mem.map_region(0, mem_size_pages, Access::ReadWrite, None)
183 .map_err(VmError::MapRegion)?;
184 }
185 for overlay_entry in inst.rw_overlays.iter() {
186 overlay_into(
187 &mut mem,
188 overlay_entry.start,
189 overlay_entry.bytes.as_slice(),
190 Access::ReadWrite,
191 )?;
192 }
193
194 let mut regs = Regs::new();
197 regs.pc = endpoint.entry_pc;
198 regs.gpr = endpoint.initial_regs;
199 for (i, v) in inst.regs.iter().enumerate() {
200 if *v != 0 {
201 regs.gpr[i] = *v;
202 }
203 }
204 for (i, v) in args.iter().enumerate() {
205 regs.gpr[7 + i] = *v;
206 }
207
208 let gas = GasCounter::new(gas_budget);
213 let gas_initial = gas_budget;
214
215 let pinned_slots: Vec<SlotIdx> = img.pinned.iter().map(|e| e.slot).collect();
218
219 let entry = InstanceEntry {
220 instance_ref: inst_ref,
221 image_hash_chain: inst.image_hash_chain,
222 image_hash: inst.image_hash,
223 program,
224 root_cnode,
225 yield_marker_slot: img.yield_marker_slot,
226 pinned_slots,
227 regs: Regs::new(), mem: Mem::new(), gas: GasCounter::new(0), status: EntryStatus::Waiting,
231 };
232 Ok((entry, mem, regs, gas, gas_initial))
233 }
234
235 pub fn call_resume(
249 &mut self,
250 cache: &mut CacheDirectory,
251 scratchpad: Option<CapHashOrRef>,
252 ) -> Result<CallResult, VmError> {
253 match self.stack.running() {
255 Some(Entry::Reference(_)) => {}
256 _ => {
257 return Err(VmError::Invariant(
258 "call_resume: top is not a ReferenceEntry",
259 ));
260 }
261 }
262 self.stack.pop().ok_or(VmError::CallStackEmpty)?;
263
264 let pos = self.stack.entries().len() - 1;
267 if let Some(target) = scratchpad {
268 let inst = self
269 .stack
270 .running_instance_mut()
271 .ok_or(VmError::Invariant("call_resume: no instance after pop"))?;
272 inst.root_cnode.set(SlotIdx(0), Some(target))?;
273 }
274
275 let (regs, mem, gas, gas_initial) = {
279 let target = self
280 .stack
281 .running_instance_mut()
282 .ok_or(VmError::Invariant("call_resume: no instance"))?;
283 let regs = core::mem::replace(&mut target.regs, Regs::new());
284 let mem = core::mem::replace(&mut target.mem, Mem::new());
285 let gas = core::mem::replace(&mut target.gas, GasCounter::new(0));
286 let gas_initial = gas.remaining();
287 (regs, mem, gas, gas_initial)
288 };
289
290 self.drive_and_translate(cache, regs, mem, gas, gas_initial, pos)
291 }
292
293 pub fn drop_paused(&mut self, _target_slot: javm_cap::SlotPath) -> Result<(), VmError> {
296 Err(VmError::Invariant(
297 "DROP_PAUSED requires σ-resident Paused state (Stage 4)",
298 ))
299 }
300
301 fn drive_and_translate(
312 &mut self,
313 cache: &mut CacheDirectory,
314 mut regs: Regs,
315 mut mem: Mem,
316 mut gas: GasCounter,
317 gas_initial: u64,
318 pushed_pos: usize,
319 ) -> Result<CallResult, VmError> {
320 let mut cur_pos = pushed_pos;
328 let exit = loop {
329 let program = match &self.stack.entries()[cur_pos] {
330 Entry::Instance(e) => e.program.clone(),
331 _ => return Err(VmError::Invariant("cur_pos points at non-Instance")),
332 };
333 let mut handler = CachedEcallHandler { vm: self, cache };
334 let exit = Interpreter::run(
335 program.as_ref(),
336 &mut regs,
337 &mut mem,
338 &mut gas,
339 &mut handler,
340 );
341
342 if matches!(exit, ExitReason::HostCall(op) if op == host_op::SET_IMAGE) {
347 continue;
348 }
349
350 if matches!(exit, ExitReason::HostCall(op) if op == host_op::HOST_CALL) {
356 if self.stack.entries().len() != cur_pos + 2 {
357 return Err(VmError::Invariant(
358 "HOST_CALL exit without expected child push",
359 ));
360 }
361 if let Some(Entry::Instance(parent)) = self.stack.entries_mut().get_mut(cur_pos) {
362 parent.regs = regs;
363 parent.mem = mem;
364 }
365 cur_pos += 1;
366 let child = self
367 .stack
368 .running_instance_mut()
369 .ok_or(VmError::Invariant("no child after HOST_CALL push"))?;
370 regs = core::mem::replace(&mut child.regs, Regs::new());
371 mem = core::mem::replace(&mut child.mem, Mem::new());
372 continue;
373 }
374
375 if matches!(exit, ExitReason::Halt) && cur_pos > pushed_pos {
380 let child_slot0 = self
381 .stack
382 .running_instance_mut()
383 .ok_or(VmError::Invariant("no child to halt"))?
384 .root_cnode
385 .take(SlotIdx(0))
386 .ok()
387 .flatten();
388 self.stack.pop();
389 cur_pos -= 1;
390 let parent = self
391 .stack
392 .running_instance_mut()
393 .ok_or(VmError::Invariant("parent gone after child pop"))?;
394 regs = core::mem::replace(&mut parent.regs, Regs::new());
395 mem = core::mem::replace(&mut parent.mem, Mem::new());
396 if let Some(s0) = child_slot0 {
397 parent.root_cnode.set(SlotIdx(0), Some(s0))?;
398 }
399 continue;
400 }
401
402 break exit;
403 };
404
405 let gas_used = gas_initial.saturating_sub(gas.remaining());
406
407 let oog_marker_payload = if matches!(exit, ExitReason::OutOfGas) {
414 self.reconcile_and_route_oog(pushed_pos)
415 } else {
416 None
417 };
418
419 let yielded = self.stack.entries().len() > pushed_pos + 1
421 && matches!(self.stack.running(), Some(Entry::Reference(_)));
422
423 if yielded {
424 let marker_payload = if let Some(p) = oog_marker_payload {
429 Some(p)
430 } else {
431 let marker_slot = SlotIdx((regs.gpr[7] & 0xFF) as u32);
432 let yielder = match &self.stack.entries()[pushed_pos] {
433 Entry::Instance(e) => e.as_ref(),
434 _ => return Err(VmError::Invariant("yielder is not an Instance")),
435 };
436 yielder.root_cnode.get(marker_slot)
437 };
438
439 let yielder = match &mut self.stack.entries_mut()[pushed_pos] {
441 Entry::Instance(e) => e.as_mut(),
442 _ => return Err(VmError::Invariant("yielder is not an Instance")),
443 };
444 yielder.regs = regs;
445 yielder.mem = mem;
446 yielder.gas = gas;
447
448 return Ok(CallResult::Paused {
449 marker_payload,
450 gas_used,
451 });
452 }
453
454 if let Some(top) = self.stack.running_instance_mut() {
456 top.regs = regs;
457 top.mem = mem;
458 top.gas = gas;
459 }
460
461 let popped = self
465 .stack
466 .pop()
467 .ok_or(VmError::Invariant("stack empty after Interpreter::run"))?;
468
469 let (entry, slot0_target) = match popped {
470 Entry::Instance(e) => {
471 let mut e = *e;
472 let slot0 = e.root_cnode.take(SlotIdx(0)).ok().flatten();
473 (e, slot0)
474 }
475 _ => return Err(VmError::Invariant("popped a non-Instance entry")),
476 };
477
478 let post_instance_hash = if matches!(exit, ExitReason::Halt) {
479 self.settle_post_instance(cache, &entry)?
480 } else {
481 match entry.instance_ref {
485 CapHashOrRef::Hash(h) => h,
486 CapHashOrRef::Ref(_) => [0u8; 32],
487 }
488 };
489
490 Ok(match exit {
491 ExitReason::Halt => CallResult::Halt {
492 return_value: entry.regs.gpr[7],
493 post_instance_hash,
494 reflected_slot0: slot0_target,
495 gas_used,
496 },
497 ExitReason::HostCall(_) | ExitReason::Ecall => CallResult::Paused {
498 marker_payload: slot0_target,
499 gas_used,
500 },
501 ExitReason::Trap
502 | ExitReason::Panic
503 | ExitReason::OutOfGas
504 | ExitReason::PageFault(_) => CallResult::Faulted {
505 reason: exit,
506 reflected_slot0: slot0_target,
507 gas_used,
508 },
509 })
510 }
511
512 fn settle_post_instance(
517 &mut self,
518 cache: &mut CacheDirectory,
519 entry: &InstanceEntry,
520 ) -> Result<CapHash, VmError> {
521 let cnode_hash = {
525 let mut cnode = javm_cap::CNodeCap::new(entry.root_cnode.size_log)?;
526 for (idx, mo) in entry.root_cnode.slots.iter() {
527 if let ssz::MissingOr::Materialized(t) = mo {
528 cnode.set(SlotIdx(idx as u32), Some(t.clone()))?;
529 }
530 }
531 cache.put_cap(&Cap::CNode(cnode))?
532 };
533
534 let image_cap = cache
540 .get(CapHashOrRef::Hash(entry.image_hash))
541 .ok_or(VmError::ImageNotFound)?;
542 let img = match &*image_cap {
543 Cap::Image(i) => i.clone(),
544 _ => return Err(VmError::ImageNotFound),
545 };
546 let mut overlay_bufs: Vec<(u32, Vec<u8>)> = Vec::new();
547 for m in img.mappings.iter() {
548 let start = m.start as u32;
551 let len = m.size as usize;
552 if let Ok(bytes) = entry.mem.read(start, len) {
553 overlay_bufs.push((start, bytes));
554 }
555 }
556 let overlays_borrowed: Vec<(u32, &[u8])> = overlay_bufs
557 .iter()
558 .map(|(s, b)| (*s, b.as_slice()))
559 .collect();
560
561 let mem_size = if let Some(last) = img.mappings.last() {
562 (last.start + last.size) as u32
563 } else {
564 0
565 };
566
567 let hash = cache.put_cap(&Cap::instance_with_overlays(
568 entry.image_hash_chain,
569 entry.image_hash,
570 cnode_hash,
571 &overlays_borrowed,
572 mem_size,
573 entry.regs.gpr,
574 entry.regs.pc,
575 entry.gas.remaining(),
576 ))?;
577 Ok(hash)
578 }
579
580 fn reconcile_and_route_oog(&mut self, yielder_pos: usize) -> Option<CapHashOrRef> {
587 let oog_hash = kernel_image_hash(KernelImage::OogMarker);
594
595 let stack_len = self.stack.entries().len();
598 let mut target_pos: Option<usize> = None;
599 for pos in (0..yielder_pos).rev() {
600 let ie = match &self.stack.entries()[pos] {
601 Entry::Instance(ie) => ie.as_ref(),
602 Entry::Reference(_) => continue,
603 };
604 let Some(catcher_slot) = ie.yield_marker_slot else {
605 continue;
606 };
607 let catcher_hash = match ie.root_cnode.get(catcher_slot) {
612 Some(CapHashOrRef::Hash(h)) => h,
613 _ => continue,
614 };
615 let markers = self.kernel_assist.yield_catcher_markers(catcher_hash);
616 if markers.contains(&oog_hash) {
617 target_pos = Some(pos);
618 break;
619 }
620 }
621
622 let _ = stack_len;
625 match target_pos {
626 Some(pos) => {
627 self.stack.push_reference(pos).ok()?;
628 Some(CapHashOrRef::Hash(oog_hash))
629 }
630 None => None,
631 }
632 }
633}
634
635fn page_round_up_u64(n: u64) -> u64 {
637 let p = javm_exec::PAGE_SIZE as u64;
638 n.div_ceil(p) * p
639}
640
641fn overlay_into(
644 mem: &mut CopyingMemory,
645 start: u32,
646 data: &[u8],
647 access: Access,
648) -> Result<(), VmError> {
649 if data.is_empty() {
650 return Ok(());
651 }
652 let size = page_round_up_u64(data.len() as u64);
653 mem.map_region(start as u64, size, access, Some(data))
654 .map_err(VmError::MapRegion)
655}
656
657impl<K: KernelAssist + std::fmt::Debug> std::fmt::Debug for Vm<K> {
658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659 f.debug_struct("Vm")
660 .field("stack", &self.stack)
661 .field("kernel_assist", &self.kernel_assist)
662 .field("image_cache_len", &self.image_cache.len())
663 .finish()
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use crate::kernel_assist::InProcessKernelAssist;
671 use javm_cap::image::Image;
672 use javm_cap::{CacheDirectory, Cap, NUM_REGS};
673 use std::collections::BTreeMap;
674
675 fn empty_image_with_code(code: Vec<u8>) -> Image {
676 let packed_bitmask = vec![0xFFu8; code.len().div_ceil(8)];
677 Image {
678 code,
679 packed_bitmask,
680 jump_table: Vec::new(),
681 endpoints: BTreeMap::new(),
682 memory_mappings: Vec::new(),
683 gas_slots: Vec::new(),
684 quota_slots: Vec::new(),
685 pinned_slots: BTreeMap::new(),
686 initial_slots: BTreeMap::new(),
687 yield_marker_slot: None,
688 }
689 }
690
691 fn publish_simple_instance(cache: &mut CacheDirectory, image: Image) -> CapHash {
694 let image_hash = cache
695 .put_cap(&Cap::image_with_slots(&image, &[], &[]).unwrap())
696 .unwrap();
697 let cnode_hash = cache.put_cap(&Cap::empty_cnode(8).unwrap()).unwrap();
698 cache
699 .put_cap(&Cap::instance_with_overlays(
700 [0xAA; 32],
701 image_hash,
702 cnode_hash,
703 &[],
704 0,
705 [0u64; NUM_REGS],
706 0,
707 0,
708 ))
709 .unwrap()
710 }
711
712 #[test]
713 fn new_constructs_empty_vm() {
714 let vm = Vm::new(InProcessKernelAssist::new());
715 assert!(vm.stack.is_empty());
716 assert!(vm.image_cache.is_empty());
717 }
718
719 #[test]
720 fn invoke_cached_trap_returns_faulted() {
721 let img = empty_image_with_code(vec![0u8]);
723 let mut cache = CacheDirectory::new();
724 let inst_hash = publish_simple_instance(&mut cache, img);
725
726 let mut vm = Vm::new(InProcessKernelAssist::new());
727 let r = vm
728 .invoke_cached(&mut cache, inst_hash, 0, [0; 4], 1000)
729 .unwrap();
730 assert!(matches!(
731 r,
732 CallResult::Faulted {
733 reason: ExitReason::Trap,
734 ..
735 }
736 ));
737 assert!(vm.stack.is_empty());
738 }
739
740 #[test]
741 fn invoke_cached_ecalli_zero_halts() {
742 let mut img = empty_image_with_code(vec![10u8, 0]);
745 img.packed_bitmask = vec![0b01u8];
746 let mut cache = CacheDirectory::new();
747 let inst_hash = publish_simple_instance(&mut cache, img);
748
749 let mut vm = Vm::new(InProcessKernelAssist::new());
750 let r = vm
751 .invoke_cached(&mut cache, inst_hash, 0, [0; 4], 1000)
752 .unwrap();
753 assert!(matches!(
754 r,
755 CallResult::Halt {
756 return_value: 0,
757 ..
758 }
759 ));
760 assert!(vm.stack.is_empty());
761 }
762
763 #[test]
764 fn invoke_cached_load_imm_then_reply() {
765 let mut code = Vec::new();
768 let mut bitmask_unpacked = Vec::new();
769 code.extend_from_slice(&[20u8, 7]);
770 bitmask_unpacked.extend_from_slice(&[1u8, 0]);
771 for i in 0..8 {
772 code.push(if i == 0 { 42 } else { 0 });
773 bitmask_unpacked.push(0);
774 }
775 code.extend_from_slice(&[10u8, 0]);
776 bitmask_unpacked.extend_from_slice(&[1u8, 0]);
777
778 let mut packed = vec![0u8; bitmask_unpacked.len().div_ceil(8)];
780 for (i, b) in bitmask_unpacked.iter().enumerate() {
781 if *b != 0 {
782 packed[i / 8] |= 1 << (i % 8);
783 }
784 }
785
786 let img = Image {
787 code,
788 packed_bitmask: packed,
789 jump_table: Vec::new(),
790 endpoints: BTreeMap::new(),
791 memory_mappings: Vec::new(),
792 gas_slots: Vec::new(),
793 quota_slots: Vec::new(),
794 pinned_slots: BTreeMap::new(),
795 initial_slots: BTreeMap::new(),
796 yield_marker_slot: None,
797 };
798
799 let mut cache = CacheDirectory::new();
800 let inst_hash = publish_simple_instance(&mut cache, img);
801
802 let mut vm = Vm::new(InProcessKernelAssist::new());
803 let r = vm
804 .invoke_cached(&mut cache, inst_hash, 0, [0; 4], 1000)
805 .unwrap();
806 match r {
807 CallResult::Halt {
808 return_value,
809 gas_used,
810 ..
811 } => {
812 assert_eq!(return_value, 42);
813 assert!(gas_used > 0);
814 }
815 other => panic!("expected Halt, got {:?}", other),
816 }
817 }
818
819 #[test]
827 fn invoke_cached_host_call_into_child_then_halts() {
828 let s_img = {
831 let mut code = Vec::new();
832 let mut bm = Vec::new();
833 code.extend_from_slice(&[20u8, 7]);
834 bm.extend_from_slice(&[1u8, 0]);
835 for i in 0..8 {
836 code.push(if i == 0 { 42 } else { 0 });
837 bm.push(0);
838 }
839 code.extend_from_slice(&[10u8, 0]);
840 bm.extend_from_slice(&[1u8, 0]);
841 let mut packed = vec![0u8; bm.len().div_ceil(8)];
842 for (i, b) in bm.iter().enumerate() {
843 if *b != 0 {
844 packed[i / 8] |= 1 << (i % 8);
845 }
846 }
847 Image {
848 code,
849 packed_bitmask: packed,
850 jump_table: Vec::new(),
851 endpoints: BTreeMap::new(),
852 memory_mappings: Vec::new(),
853 gas_slots: Vec::new(),
854 quota_slots: Vec::new(),
855 pinned_slots: BTreeMap::new(),
856 initial_slots: BTreeMap::new(),
857 yield_marker_slot: None,
858 }
859 };
860
861 let m_img = {
864 let mut code = Vec::new();
865 let mut bm = Vec::new();
866 code.extend_from_slice(&[20u8, 7]);
868 bm.extend_from_slice(&[1u8, 0]);
869 for i in 0..8 {
870 code.push(if i == 0 { 9 } else { 0 });
871 bm.push(0);
872 }
873 code.extend_from_slice(&[20u8, 8]);
875 bm.extend_from_slice(&[1u8, 0]);
876 for _ in 0..8 {
877 code.push(0);
878 bm.push(0);
879 }
880 code.extend_from_slice(&[10u8, 26]);
882 bm.extend_from_slice(&[1u8, 0]);
883 code.extend_from_slice(&[10u8, 0]);
885 bm.extend_from_slice(&[1u8, 0]);
886 let mut packed = vec![0u8; bm.len().div_ceil(8)];
887 for (i, b) in bm.iter().enumerate() {
888 if *b != 0 {
889 packed[i / 8] |= 1 << (i % 8);
890 }
891 }
892 Image {
893 code,
894 packed_bitmask: packed,
895 jump_table: Vec::new(),
896 endpoints: BTreeMap::new(),
897 memory_mappings: Vec::new(),
898 gas_slots: Vec::new(),
899 quota_slots: Vec::new(),
900 pinned_slots: BTreeMap::new(),
901 initial_slots: BTreeMap::new(),
902 yield_marker_slot: None,
903 }
904 };
905
906 let mut cache = CacheDirectory::new();
908 let s_inst_hash = publish_simple_instance(&mut cache, s_img);
909
910 let m_image_hash = cache
912 .put_cap(&Cap::image_with_slots(&m_img, &[], &[]).unwrap())
913 .unwrap();
914 let m_cnode_hash = {
915 let mut cn = javm_cap::CNodeCap::new(8).unwrap();
916 cn.set(SlotIdx(9), Some(CapHashOrRef::Hash(s_inst_hash)))
917 .unwrap();
918 cache.put_cap(&Cap::CNode(cn)).unwrap()
919 };
920 let m_inst_hash = cache
921 .put_cap(&Cap::instance_with_overlays(
922 [0xAA; 32],
923 m_image_hash,
924 m_cnode_hash,
925 &[],
926 0,
927 [0u64; NUM_REGS],
928 0,
929 0,
930 ))
931 .unwrap();
932
933 let mut vm = Vm::new(InProcessKernelAssist::new());
934 let r = vm
935 .invoke_cached(&mut cache, m_inst_hash, 0, [0; 4], 100_000)
936 .unwrap();
937 assert!(
938 matches!(r, CallResult::Halt { .. }),
939 "expected Halt, got {:?}",
940 r,
941 );
942 assert!(vm.stack.is_empty());
943 }
944
945 #[test]
946 fn invoke_cached_oog_returns_faulted() {
947 let code = vec![1u8; 50];
949 let mut packed = vec![0xFFu8; code.len().div_ceil(8)];
950 if !code.len().is_multiple_of(8) {
951 let used = code.len() % 8;
953 let last = packed.len() - 1;
954 packed[last] = (1u8 << used) - 1;
955 }
956 let img = Image {
957 code,
958 packed_bitmask: packed,
959 jump_table: Vec::new(),
960 endpoints: BTreeMap::new(),
961 memory_mappings: Vec::new(),
962 gas_slots: Vec::new(),
963 quota_slots: Vec::new(),
964 pinned_slots: BTreeMap::new(),
965 initial_slots: BTreeMap::new(),
966 yield_marker_slot: None,
967 };
968 let mut cache = CacheDirectory::new();
969 let inst_hash = publish_simple_instance(&mut cache, img);
970 let mut vm = Vm::new(InProcessKernelAssist::new());
971 let r = vm
972 .invoke_cached(&mut cache, inst_hash, 0, [0; 4], 3)
973 .unwrap();
974 assert!(matches!(
975 r,
976 CallResult::Faulted {
977 reason: ExitReason::OutOfGas,
978 ..
979 }
980 ));
981 }
982}