corevm_engine/
engine.rs

1use crate::{
2	hash_encoded, FatalError, FsNode, HostCallError, HostCallHandler, InitError, InnerVm,
3	InvokeArgs, KernelContext, Lookup, MemoryManager, OpenedFile, OuterVm, ProgramData, Reg,
4	RunError, TouchError,
5};
6use alloc::vec::Vec;
7use codec::{Compact, CompactLen, ConstEncodedLen, DecodeAll, Encode};
8use corevm_host::{
9	fs, AudioMode, CoreVmOutput, CoreVmPayload, ExecEnv, KernelFd, Outcome, RangeSet, VideoMode,
10	VmOutput, VmState, PAGE_SIZE,
11};
12use jam_pvm_common::{ApiError, InvokeOutcome};
13use jam_types::Hash;
14use log::{debug, trace};
15use polkakernel::{KernelState, Machine};
16use polkavm::{MemoryMapBuilder, RETURN_TO_HOST};
17
18/// An engine that drives execution of `refine` CoreVM service entry point.
19pub struct Engine<O: OuterVm> {
20	pub(crate) args: InvokeArgs,
21	pub(crate) memory_man: MemoryManager<O>,
22	pub(crate) video: Option<VideoMode>,
23	pub(crate) audio: Option<AudioMode>,
24	pub(crate) exec: ExecEnv,
25	/// The number of video frames produced by the guest so far.
26	pub(crate) num_video_frames: u64,
27	/// In bytes.
28	/// Total size of audio frames produced by the guest so far in bytes.
29	pub(crate) num_audio_bytes: u64,
30	exec_ref: fs::BlockRef,
31	old_hash: Hash,
32	host_call_handlers: Vec<HostCallHandler<O>>,
33	/// The index of the host-call being handled.
34	current_host_call: Option<u64>,
35}
36
37impl<O: OuterVm> Engine<O> {
38	pub fn new(payload: CoreVmPayload, mut outer_vm: O) -> Result<Self, InitError> {
39		let CoreVmPayload { gas, vm_state, exec_ref } = payload;
40		let exec = outer_vm.read_file(&exec_ref)?;
41		let exec = ExecEnv::decode_all(&mut &exec[..]).map_err(|_| InitError::ExecBlob)?;
42		let max_exports = outer_vm.get_export_count();
43		let auth_output_len = outer_vm.get_auth_output_len();
44		let mut program_counter = vm_state.program_counter;
45		let program = {
46			let bytes = outer_vm.read_file(&exec.program)?;
47			let corevm_blob = jam_program_blob_common::CoreVmProgramBlob::from_bytes(&bytes)
48				.ok_or(InitError::ProgramBlob)?;
49			polkavm::ProgramParts::from_bytes(corevm_blob.pvm_blob.into())?
50		};
51		let memory_map = MemoryMapBuilder::new(PAGE_SIZE as u32)
52			.ro_data_size(program.ro_data_size)
53			.rw_data_size(program.rw_data_size)
54			.stack_size(program.stack_size)
55			.build()
56			.map_err(|_| InitError::ProgramBlob)?;
57		let code_and_jump_table = program.code_and_jump_table.clone();
58		let ro_data = program.ro_data.clone();
59		let rw_data = program.rw_data.clone();
60		let program_blob =
61			polkavm::ProgramBlob::from_parts(program).map_err(|_| InitError::ProgramBlob)?;
62		let host_call_handlers = Self::create_host_call_handlers(&program_blob);
63		// Should include program hash and pages' hash.
64		let old_hash = compute_state_hash(&vm_state);
65		let mut args = InvokeArgs { regs: vm_state.regs, gas };
66		let initial_program_run = program_counter == 0;
67		let mut libc = false;
68		if initial_program_run {
69			debug!("This is initial program run");
70			program_counter = program_blob
71				.exports()
72				.find(|export| {
73					let name = export.symbol().as_bytes();
74					if name == b"main" {
75						debug!("Found `main` (no libc) entry point");
76						return true;
77					}
78					if name == b"_pvm_start" {
79						debug!("Found `_pvm_start` (libc) entry point");
80						libc = true;
81						return true;
82					}
83					false
84				})
85				.expect("Neither `main` nor `_pvm_start` entry point found")
86				.program_counter()
87				.0
88				.into();
89		}
90		let inner_vm = outer_vm
91			.machine(&code_and_jump_table[..], program_counter)
92			.expect("Failed to load the code");
93		let program_data = ProgramData::new(&memory_map, ro_data, rw_data);
94		let kernel_state = KernelState {
95			fds: vm_state
96				.kernel
97				.fds
98				.into_iter()
99				.map(|(fd, file)| {
100					// TODO @ivan We probably want to open and read files lazily.
101					let (block_ref, position) = (file.block_ref, file.position);
102					let mut lookup = Lookup { outer_vm: &mut outer_vm };
103					let node = fs::Node::open(&block_ref, &mut lookup)?;
104					let node = match node {
105						fs::Node::File(mut file) => {
106							file.seek(position)?;
107							FsNode::File(file)
108						},
109						fs::Node::Dir(dir) => FsNode::Dir { dir, position },
110					};
111					let opened_file = OpenedFile { block_ref, node };
112					Ok((fd, opened_file))
113				})
114				.collect::<Result<_, fs::Error>>()?,
115		};
116		let mut memory_man = MemoryManager::new(
117			outer_vm,
118			inner_vm,
119			program_data,
120			vm_state.mapped_heap_pages,
121			vm_state.resident_pages,
122			max_exports as usize,
123			auth_output_len as usize,
124			kernel_state,
125		)
126		.expect("Failed to initialize memory pages");
127		if initial_program_run {
128			// Initialize the registers for the initial `invoke` call.
129			let default_sp = memory_map.stack_address_high() as u64;
130			if libc {
131				let unused = fs::BlockRef { service_id: 0, hash: Default::default() };
132				let mut context = KernelContext {
133					memory_man: &mut memory_man,
134					regs: &mut args.regs,
135					root_dir: unused,
136					error: None,
137				};
138				context
139					.init(
140						default_sp,
141						RETURN_TO_HOST,
142						exec.args.iter().map(|a| a.as_ref()),
143						exec.env.iter().map(|a| a.as_ref()),
144					)
145					.expect("Failed to init arguments");
146			} else {
147				args.set_reg(Reg::RA, RETURN_TO_HOST);
148				args.set_reg(Reg::SP, default_sp);
149			}
150		}
151		Ok(Self {
152			args,
153			memory_man,
154			video: vm_state.video,
155			audio: vm_state.audio,
156			old_hash,
157			num_video_frames: 0,
158			num_audio_bytes: 0,
159			exec,
160			exec_ref,
161			host_call_handlers,
162			current_host_call: vm_state.restart_host_call,
163		})
164	}
165
166	pub fn run(mut self) -> Result<(CoreVmOutput, O), RunError> {
167		// Restart the host-call that the engine was executing while ran out of output
168		// space. Panic if the host-call needs to be restarted again.
169		let mut restart_host_call = self.current_host_call.is_some();
170		let (output, outer_vm) = loop {
171			if let Some(index) = self.current_host_call.take() {
172				use Outcome::*;
173				if restart_host_call {
174					debug!("Restarting host-call {index}");
175				}
176				let outcome = match self.handle_host_call_fault(index) {
177					Ok(()) => {
178						// Continue normal execution.
179						None
180					},
181					Err(HostCallError::Outcome(
182						outcome @ Panic | outcome @ Halt | outcome @ OutOfGas,
183					)) => Some(outcome),
184					Err(HostCallError::Outcome(outcome @ TimeLimitReached)) => {
185						// NOTE We don't restart the host-call in this case.
186						debug!("Time limit reached in host-call {index}");
187						Some(outcome)
188					},
189					Err(HostCallError::Outcome(outcome @ OutputLimitReached)) => {
190						if restart_host_call {
191							return Err(FatalError::NotEnoughOutputSpace.into());
192						}
193						debug!("Output limit reached in host-call {index}");
194						self.current_host_call = Some(index);
195						Some(outcome)
196					},
197					Err(HostCallError::Outcome(outcome @ PageFault { page, num_pages })) => {
198						if restart_host_call {
199							debug!(
200								"Hard page fault at {:#x?} while restarting host-call {index}",
201								page..page + num_pages
202							);
203							// NOTE: This error might also occur if the builder haven't imported all
204							// the necessary pages. Normally this shouldn't happen.
205							return Err(FatalError::NotEnoughInputSpace.into());
206						}
207						debug!(
208							"Hard page fault at {:#x?} in host-call {index}",
209							page..page + num_pages
210						);
211						self.current_host_call = Some(index);
212						Some(outcome)
213					},
214					Err(HostCallError::Jam(e)) => return Err(e.into()),
215					Err(HostCallError::Fs(e)) => return Err(e.into()),
216					Err(HostCallError::Fatal(e)) => return Err(e.into()),
217				};
218				if let Some(outcome) = outcome {
219					break self.into_host_output(outcome)?;
220				}
221				restart_host_call = false;
222			}
223			let (outcome, gas, regs) =
224				self.memory_man.inner_vm.invoke(self.args.gas, self.args.regs)?;
225			trace!("Invoke outcome {:?}", DebugInvokeOutcome(outcome));
226			self.args.gas = gas;
227			self.args.regs = regs;
228			match outcome {
229				InvokeOutcome::Halt => {
230					break self.into_host_output(Outcome::Halt)?;
231				},
232				InvokeOutcome::PageFault(address) => {
233					match self.memory_man.touch(address) {
234						Ok(..) => {},
235						Err(TouchError::PageFault { page, num_pages }) => {
236							trace!("Hard page fault at address {:#x}", page * PAGE_SIZE);
237							// Suspend the execution when _either_ the program tries to
238							// access the page that was not imported _or_
239							// the max. no. of allocated pages is reached.
240							break self.into_host_output(Outcome::PageFault { page, num_pages })?;
241						},
242						Err(TouchError::Jam(e)) => return Err(e.into()),
243					}
244				},
245				InvokeOutcome::HostCallFault(index) => self.current_host_call = Some(index),
246				InvokeOutcome::Panic => {
247					break self.into_host_output(Outcome::Panic)?;
248				},
249				InvokeOutcome::OutOfGas => {
250					break self.into_host_output(Outcome::OutOfGas)?;
251				},
252			}
253		};
254		debug!("Finished with outcome {:?}", output.vm_output.outcome);
255		Ok((output, outer_vm))
256	}
257
258	fn handle_host_call_fault(&mut self, index: u64) -> Result<(), HostCallError> {
259		let Some(handler) = self.host_call_handlers.get(index as usize) else {
260			panic!("Unknown host call index: {index}");
261		};
262		handler(self)
263	}
264
265	fn into_host_output(self, outcome: Outcome) -> Result<(CoreVmOutput, O), ApiError> {
266		let (
267			outer_vm,
268			inner_vm,
269			mapped_heap_pages,
270			resident_pages,
271			touched_imported_pages,
272			updated_pages,
273			num_memory_pages,
274			stream_len,
275			kernel_state,
276		) = self.memory_man.export()?;
277		let program_counter = inner_vm.expunge().expect("Failed to close VM handle");
278		let vm_state = VmState {
279			mapped_heap_pages,
280			resident_pages,
281			regs: self.args.regs,
282			program_counter,
283			kernel: kernel_state,
284			restart_host_call: self.current_host_call,
285			video: self.video,
286			audio: self.audio,
287		};
288		let new_hash = compute_state_hash(&vm_state);
289		let work_output = CoreVmOutput {
290			vm_output: VmOutput {
291				remaining_gas: self.args.gas,
292				outcome,
293				num_memory_pages,
294				stream_len,
295			},
296			vm_state,
297			old_hash: self.old_hash,
298			new_hash,
299			touched_imported_pages,
300			updated_pages,
301			exec_ref: self.exec_ref,
302		};
303		Ok((work_output, outer_vm))
304	}
305}
306
307/// This hash is used to check the validity of the state transition in JAM's `accumulate`.
308pub fn compute_state_hash(vm: &VmState) -> Hash {
309	hash_encoded((&vm.regs, vm.program_counter, &vm.mapped_heap_pages, &vm.resident_pages))
310}
311
312/// The hash of the initial state.
313pub fn initial_state_hash() -> Hash {
314	let initial_vm_state = VmState::initial();
315	compute_state_hash(&initial_vm_state)
316}
317
318pub(crate) struct DebugInvokeOutcome(pub InvokeOutcome);
319
320impl core::fmt::Debug for DebugInvokeOutcome {
321	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
322		match self.0 {
323			InvokeOutcome::Halt => f.write_str("Halt"),
324			InvokeOutcome::PageFault(address) =>
325				f.debug_tuple("PageFault").field(&format_args!("{address:#x}")).finish(),
326			InvokeOutcome::HostCallFault(i) => f.debug_tuple("HostCallFault").field(&i).finish(),
327			InvokeOutcome::Panic => f.write_str("Panic"),
328			InvokeOutcome::OutOfGas => f.write_str("OutOfGas"),
329		}
330	}
331}
332
333pub(crate) fn get_work_output_len(
334	mapped_heap_pages: &RangeSet,
335	resident_pages: &RangeSet,
336	num_imported_pages: usize,
337	num_updated_pages: usize,
338	num_opened_files: usize,
339) -> usize {
340	MAX_STATIC_WORK_OUTPUT_LEN +
341		mapped_heap_pages.encoded_size() +
342		resident_pages.encoded_size() +
343		vec_map_encoded_len::<u64, Hash>(num_imported_pages) +
344		vec_map_encoded_len::<u64, Hash>(num_updated_pages) +
345		vec_map_encoded_len::<u32, KernelFd>(num_opened_files)
346}
347
348fn vec_map_encoded_len<K: ConstEncodedLen, V: ConstEncodedLen>(len: usize) -> usize {
349	(K::max_encoded_len() + V::max_encoded_len()) * len + Compact::<u64>::compact_len(&(len as u64))
350}
351
352/// Maximum length of the work output in bytes without dynamically allocated components (e.g.
353/// pages).
354const MAX_STATIC_WORK_OUTPUT_LEN: usize = 269;
355
356#[cfg(test)]
357mod tests {
358	use super::*;
359	use corevm_host::{OutputStream, Range};
360	use jam_types::{max_exports, max_imports, Encode, ServiceId, SignedGas};
361	use rand::Rng;
362
363	#[test]
364	fn max_static_work_output_len_is_correct() {
365		assert_eq!(MAX_STATIC_WORK_OUTPUT_LEN, encoded_work_output_len(max_exports()));
366	}
367
368	fn encoded_work_output_len(num_exports: u32) -> usize {
369		let output = CoreVmOutput {
370			vm_output: VmOutput {
371				remaining_gas: SignedGas::MAX,
372				outcome: Outcome::Halt,
373				num_memory_pages: num_exports,
374				stream_len: [u32::MAX; OutputStream::COUNT],
375			},
376			vm_state: VmState {
377				regs: Default::default(),
378				program_counter: Default::default(),
379				mapped_heap_pages: Default::default(),
380				resident_pages: Default::default(),
381				kernel: Default::default(),
382				restart_host_call: Some(u64::MAX),
383				video: Some(
384					VideoMode::new(1, 1, 1, corevm_host::VideoFrameFormat::Rgb888Indexed8).unwrap(),
385				),
386				audio: Some(AudioMode::new(1, 1, corevm_host::AudioSampleFormat::S16LE).unwrap()),
387			},
388			old_hash: Default::default(),
389			new_hash: Default::default(),
390			updated_pages: Default::default(),
391			touched_imported_pages: Default::default(),
392			exec_ref: fs::BlockRef { service_id: 0, hash: fs::Hash(Hash::default()) },
393		};
394		output.encode().len() -
395			output.updated_pages.encode().len() -
396			output.touched_imported_pages.encode().len() -
397			output.vm_state.mapped_heap_pages.encode().len() -
398			output.vm_state.resident_pages.encode().len() -
399			output.vm_state.kernel.fds.encode().len()
400	}
401
402	#[test]
403	fn get_work_output_len_works() {
404		let mut rng = rand::rng();
405		for num_imported_pages in [0, 1, 2, max_imports() as usize] {
406			for num_updated_pages in [0, 1, 2, max_exports() as usize] {
407				for num_opened_files in [0, 1, 2, 1000] {
408					let mapped_heap_pages = random_range_set(&mut rng);
409					let resident_pages = random_range_set(&mut rng);
410					let expected_len = get_work_output_len(
411						&mapped_heap_pages,
412						&resident_pages,
413						num_imported_pages,
414						num_updated_pages,
415						num_opened_files,
416					);
417					let actual_len = get_work_output_len_slow(
418						&mapped_heap_pages,
419						&resident_pages,
420						num_imported_pages,
421						num_updated_pages,
422						num_opened_files,
423					);
424					assert_eq!(
425						expected_len, actual_len,
426						"expected = {expected_len}, \
427                        actual = {actual_len}, \
428                        num_imported_pages = {num_imported_pages}, \
429                        num_updated_pages = {num_updated_pages}, \
430                        num_opened_files = {num_opened_files}"
431					);
432				}
433			}
434		}
435	}
436
437	fn get_work_output_len_slow(
438		mapped_heap_pages: &RangeSet,
439		resident_pages: &RangeSet,
440		num_imported_pages: usize,
441		num_updated_pages: usize,
442		num_opened_files: usize,
443	) -> usize {
444		let output = CoreVmOutput {
445			vm_output: VmOutput {
446				remaining_gas: SignedGas::MAX,
447				outcome: Outcome::Halt,
448				num_memory_pages: max_exports(),
449				stream_len: [u32::MAX; OutputStream::COUNT],
450			},
451			vm_state: VmState {
452				regs: Default::default(),
453				program_counter: Default::default(),
454				mapped_heap_pages: mapped_heap_pages.clone(),
455				resident_pages: resident_pages.clone(),
456				kernel: corevm_host::KernelState {
457					fds: (0..num_opened_files)
458						.map(|i| {
459							(
460								i as u32,
461								KernelFd {
462									block_ref: fs::BlockRef {
463										service_id: ServiceId::MAX,
464										hash: fs::Hash::default(),
465									},
466									position: u64::MAX,
467								},
468							)
469						})
470						.collect(),
471				},
472				restart_host_call: Some(u64::MAX),
473				video: Some(
474					VideoMode::new(1, 1, 1, corevm_host::VideoFrameFormat::Rgb888Indexed8).unwrap(),
475				),
476				audio: Some(AudioMode::new(1, 1, corevm_host::AudioSampleFormat::S16LE).unwrap()),
477			},
478			old_hash: Default::default(),
479			new_hash: Default::default(),
480			updated_pages: (0..num_updated_pages)
481				.map(|i| (i as u64 * PAGE_SIZE, Hash::default()))
482				.collect(),
483			touched_imported_pages: (0..num_imported_pages)
484				.map(|i| (i as u64 * PAGE_SIZE, Hash::default()))
485				.collect(),
486			exec_ref: fs::BlockRef { service_id: 0, hash: fs::Hash(Hash::default()) },
487		};
488		output.encode().len()
489	}
490
491	fn random_range_set<R: Rng>(rng: &mut R) -> RangeSet {
492		let mut ranges = RangeSet::new();
493		{
494			let mut offset = 0;
495			for _ in 0..10 {
496				let start = rng.random_range(offset..=20);
497				let end = rng.random_range(start..=20);
498				offset = end;
499				ranges.insert(Range::new(start, end));
500			}
501		}
502		ranges
503	}
504}