referrerpolicy=no-referrer-when-downgrade

sc_virtualization/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Host-side PolkaVM backend for the [`sp_virtualization`] host functions.
20//!
21//! Provides the concrete [`VirtManager`] that drives `polkavm` to compile, instantiate
22//! and execute programs on behalf of the runtime. Register it with the externalities via
23//! [`sp_virtualization::VirtManagerExt::new`].
24
25use polkavm::{
26	CacheModel, CompileError, Config, CostModelKind, Engine, GasMeteringKind, InterruptKind,
27	MemoryAccessError, Module, ModuleConfig, ProgramCounter, RawInstance, Reg,
28};
29use sp_virtualization::{
30	DestroyError, ExecBuffer, ExecError, ExecStatus, InstanceId, InstantiateError, MemoryError,
31	ModuleError, ModuleId, SyscallSymbol, VirtManagerBackend, LOG_TARGET,
32};
33use std::{
34	collections::HashMap,
35	sync::{Arc, LazyLock},
36};
37
38/// This is the single PolkaVM engine we use for everything.
39///
40/// By using a common engine we allow PolkaVM to use caching. This caching is important
41/// to reduce startup costs. This is even the case when instances use different code.
42static ENGINE: LazyLock<Engine> = LazyLock::new(|| {
43	let mut config = Config::from_env().expect("Invalid config.");
44	config.set_worker_count(10);
45	config.set_default_cost_model(Some(CostModelKind::Full(CacheModel::L2Hit)));
46	Engine::new(&config).expect("Failed to initialize PolkaVM.")
47});
48
49fn map_memory_error(error: MemoryAccessError) -> MemoryError {
50	match error {
51		MemoryAccessError::OutOfRangeAccess { .. } | MemoryAccessError::MemoryLimitReached => {
52			MemoryError::OutOfBounds
53		},
54		MemoryAccessError::Error(error) => {
55			panic!("Error accessing PolkaVM memory: {error}. This is a bug.");
56		},
57	}
58}
59
60/// A compiled module together with lookup tables derived from it once at compile time.
61///
62/// Precomputing these avoids an O(n) `exports()` scan on every `prepare` call and a
63/// re-copy of the import symbol bytes on every syscall.
64struct CompiledModule {
65	module: Module,
66	/// Export symbol → program counter. Consulted by `prepare`.
67	exports: HashMap<Vec<u8>, ProgramCounter>,
68	/// Preassembled `SyscallSymbol` for each import, indexed by hostcall index.
69	imports: Vec<SyscallSymbol>,
70}
71
72impl CompiledModule {
73	fn new(module: Module) -> Result<Self, ModuleError> {
74		let exports = module
75			.exports()
76			.map(|e| (e.symbol().as_bytes().to_vec(), e.program_counter()))
77			.collect();
78
79		// `ImportsIter` yields `Option<ProgramSymbol>` (None on malformed offsets);
80		// we also reject any symbol longer than our fixed-size `SyscallSymbol` buffer
81		// so the Ecalli hot path can just index into the vec.
82		let imports: Vec<SyscallSymbol> = module
83			.imports()
84			.into_iter()
85			.map(|symbol| {
86				let symbol = symbol.ok_or(ModuleError::InvalidImage)?;
87				SyscallSymbol::new(symbol.as_bytes()).ok_or(ModuleError::InvalidImage)
88			})
89			.collect::<Result<_, _>>()?;
90
91		Ok(Self { module, exports, imports })
92	}
93}
94
95/// The state an instance can be in.
96enum InstanceState {
97	/// Idle — awaiting a `prepare` call before it can be run.
98	Idle(RawInstance),
99	/// Ready — prepared for execution, or suspended mid-execution at a syscall.
100	Ready(RawInstance),
101}
102
103/// An instance together with the compiled module it was instantiated from.
104///
105/// The module handle is kept here so the hot paths can consult the precomputed
106/// export/import tables without going through `RawInstance::module()`.
107struct ManagedInstance {
108	state: InstanceState,
109	module: Arc<CompiledModule>,
110}
111
112/// Manages virtualization instances and their lifecycle.
113///
114/// Instance and module IDs are assigned deterministically from incrementing counters,
115/// ensuring no non-determinism across different executions.
116///
117/// NOTE: The per-instance `cache` deduplicates modules within the lifetime of one
118/// `VirtManager` (i.e. one externalities extension, i.e. one block). Cross-block
119/// reuse is deferred to PolkaVM's built-in on-disk persistent cache.
120pub struct VirtManager {
121	instances: HashMap<InstanceId, ManagedInstance>,
122	modules: HashMap<ModuleId, Arc<CompiledModule>>,
123	cache: HashMap<Vec<u8>, Arc<CompiledModule>>,
124	instance_counter: u32,
125	module_counter: u32,
126}
127
128impl Default for VirtManager {
129	fn default() -> Self {
130		Self {
131			instances: HashMap::new(),
132			modules: HashMap::new(),
133			cache: HashMap::new(),
134			instance_counter: 0,
135			module_counter: 0,
136		}
137	}
138}
139
140impl VirtManager {
141	fn next_module_id(&mut self) -> ModuleId {
142		let old = self.module_counter;
143		self.module_counter = old + 1;
144		ModuleId::from(old)
145	}
146
147	fn next_instance_id(&mut self) -> InstanceId {
148		let old = self.instance_counter;
149		self.instance_counter = old + 1;
150		InstanceId::from(old)
151	}
152
153	fn prepare_impl(
154		managed: ManagedInstance,
155		function: &[u8],
156	) -> (ManagedInstance, Result<(), ExecError>) {
157		let ManagedInstance { state, module } = managed;
158		let mut instance = match state {
159			InstanceState::Idle(i) => i,
160			ready @ InstanceState::Ready(_) => {
161				return (ManagedInstance { state: ready, module }, Err(ExecError::InvalidInstance));
162			},
163		};
164		match module.exports.get(function).copied() {
165			Some(pc) => {
166				instance.prepare_call_untyped(pc, &[]);
167				(ManagedInstance { state: InstanceState::Ready(instance), module }, Ok(()))
168			},
169			None => {
170				log::debug!(
171					target: LOG_TARGET,
172					"Export not found: {}",
173					String::from_utf8_lossy(function),
174				);
175				(
176					ManagedInstance { state: InstanceState::Idle(instance), module },
177					Err(ExecError::InvalidImage),
178				)
179			},
180		}
181	}
182
183	fn run_impl(
184		managed: ManagedInstance,
185		gas_left: i64,
186		a0: u64,
187	) -> (ManagedInstance, Result<(ExecStatus, ExecBuffer), ExecError>) {
188		let ManagedInstance { state, module } = managed;
189		let mut instance = match state {
190			InstanceState::Ready(i) => i,
191			idle @ InstanceState::Idle(_) => {
192				return (ManagedInstance { state: idle, module }, Err(ExecError::InvalidInstance));
193			},
194		};
195
196		instance.set_reg(Reg::A0, a0);
197		instance.set_gas(gas_left);
198
199		let interrupt = match instance.run() {
200			Ok(interrupt) => interrupt,
201			Err(err) => panic!("Polkavm failed during execution: {err}. This is a bug."),
202		};
203
204		match interrupt {
205			InterruptKind::Finished => {
206				let gas_left = instance.gas();
207				(
208					ManagedInstance { state: InstanceState::Idle(instance), module },
209					Ok((ExecStatus::Finished, ExecBuffer { gas_left, ..Default::default() })),
210				)
211			},
212			InterruptKind::Trap => (
213				ManagedInstance { state: InstanceState::Idle(instance), module },
214				Err(ExecError::Trap),
215			),
216			InterruptKind::NotEnoughGas => (
217				ManagedInstance { state: InstanceState::Idle(instance), module },
218				Err(ExecError::OutOfGas),
219			),
220			InterruptKind::Step | InterruptKind::Segfault(_) => {
221				unreachable!("PolkaVM is configured per config not to emit Step or Segfault; qed");
222			},
223			InterruptKind::Ecalli(hostcall_index) => {
224				let Some(syscall_symbol) = module.imports.get(hostcall_index as usize).copied()
225				else {
226					return (
227						ManagedInstance { state: InstanceState::Idle(instance), module },
228						Err(ExecError::InvalidImage),
229					);
230				};
231				let gas_left = instance.gas();
232				let a0 = instance.reg(Reg::A0);
233				let a1 = instance.reg(Reg::A1);
234				let a2 = instance.reg(Reg::A2);
235				let a3 = instance.reg(Reg::A3);
236				let a4 = instance.reg(Reg::A4);
237				let a5 = instance.reg(Reg::A5);
238				(
239					ManagedInstance { state: InstanceState::Ready(instance), module },
240					Ok((
241						ExecStatus::Syscall,
242						ExecBuffer { gas_left, syscall_symbol, a0, a1, a2, a3, a4, a5 },
243					)),
244				)
245			},
246		}
247	}
248}
249
250impl VirtManagerBackend for VirtManager {
251	fn compile_from_bytes(
252		&mut self,
253		program: &[u8],
254		identifier: Option<&[u8]>,
255	) -> Result<ModuleId, ModuleError> {
256		let mut module_config = ModuleConfig::new();
257		module_config.set_gas_metering(Some(GasMeteringKind::Sync));
258		let module =
259			Module::new(&ENGINE, &module_config, program.into()).map_err(|err| match err {
260				CompileError::ValidationFailed(err) => {
261					log::debug!(target: LOG_TARGET, "Failed to compile program: {}", err);
262					ModuleError::InvalidImage
263				},
264				CompileError::Error(err) => {
265					panic!("Polkavm failed during compilation: {err}. This is a bug.");
266				},
267			})?;
268		let compiled = Arc::new(CompiledModule::new(module)?);
269
270		let module_id = self.next_module_id();
271
272		if let Some(identifier) = identifier {
273			self.cache.insert(identifier.to_vec(), compiled.clone());
274		}
275		self.modules.insert(module_id, compiled);
276
277		Ok(module_id)
278	}
279
280	fn lookup(&mut self, identifier: &[u8]) -> Result<ModuleId, ModuleError> {
281		let compiled = self.cache.get(identifier).cloned().ok_or(ModuleError::NotCached)?;
282		let module_id = self.next_module_id();
283		self.modules.insert(module_id, compiled);
284		Ok(module_id)
285	}
286
287	fn instantiate(&mut self, module_id: ModuleId) -> Result<InstanceId, InstantiateError> {
288		let compiled = self.modules.get(&module_id).ok_or(InstantiateError::InvalidModule)?.clone();
289
290		let instance = compiled.module.instantiate().map_err(|err| {
291			log::debug!(target: LOG_TARGET, "Failed to instantiate program: {err}");
292			InstantiateError::InvalidImage
293		})?;
294
295		let instance_id = self.next_instance_id();
296
297		self.instances.insert(
298			instance_id,
299			ManagedInstance { state: InstanceState::Idle(instance), module: compiled },
300		);
301
302		Ok(instance_id)
303	}
304
305	fn prepare(&mut self, instance_id: InstanceId, function: &[u8]) -> Result<(), ExecError> {
306		let managed = self.instances.remove(&instance_id).ok_or(ExecError::InvalidInstance)?;
307		let (managed, result) = Self::prepare_impl(managed, function);
308		self.instances.insert(instance_id, managed);
309		result
310	}
311
312	fn run(
313		&mut self,
314		instance_id: InstanceId,
315		gas_left: i64,
316		a0: u64,
317	) -> Result<(ExecStatus, ExecBuffer), ExecError> {
318		let managed = self.instances.remove(&instance_id).ok_or(ExecError::InvalidInstance)?;
319		let (managed, result) = Self::run_impl(managed, gas_left, a0);
320		self.instances.insert(instance_id, managed);
321		result
322	}
323
324	fn destroy(&mut self, instance_id: InstanceId) -> Result<(), DestroyError> {
325		if self.instances.remove(&instance_id).is_some() {
326			Ok(())
327		} else {
328			Err(DestroyError::InvalidInstance)
329		}
330	}
331
332	fn read_memory(
333		&mut self,
334		instance_id: InstanceId,
335		offset: u32,
336		dest: &mut [u8],
337	) -> Result<(), MemoryError> {
338		let Some(ManagedInstance { state: InstanceState::Ready(instance), .. }) =
339			self.instances.get_mut(&instance_id)
340		else {
341			return Err(MemoryError::InvalidInstance);
342		};
343		instance.read_memory_into(offset, dest).map(|_| ()).map_err(map_memory_error)
344	}
345
346	fn write_memory(
347		&mut self,
348		instance_id: InstanceId,
349		offset: u32,
350		src: &[u8],
351	) -> Result<(), MemoryError> {
352		let Some(ManagedInstance { state: InstanceState::Ready(instance), .. }) =
353			self.instances.get_mut(&instance_id)
354		else {
355			return Err(MemoryError::InvalidInstance);
356		};
357		instance.write_memory(offset, src).map_err(map_memory_error)
358	}
359}
360
361#[cfg(test)]
362mod tests {
363	use super::*;
364
365	/// Two `VirtManager` instances must not share any cache state — confirms the cache lives on
366	/// the struct, not in process-global storage.
367	#[test]
368	fn cache_does_not_leak_between_instances() {
369		let program = sp_virtualization_test_fixture::binary();
370		let key: &[u8] = b"some-key";
371
372		let mut a = VirtManager::default();
373		a.compile_from_bytes(program, Some(key)).unwrap();
374		assert!(matches!(a.lookup(key), Ok(_)));
375
376		let mut b = VirtManager::default();
377		assert!(matches!(b.lookup(key), Err(ModuleError::NotCached)));
378	}
379
380	/// Passing `None` to `compile_from_bytes` must not populate the cache.
381	#[test]
382	fn compile_from_bytes_none_skips_cache() {
383		let program = sp_virtualization_test_fixture::binary();
384		let key: &[u8] = b"would-be-key";
385
386		let mut m = VirtManager::default();
387		m.compile_from_bytes(program, None).unwrap();
388		assert!(matches!(m.lookup(key), Err(ModuleError::NotCached)));
389	}
390}