1use blake2::{digest::consts::U5, Blake2b, Digest};
2use std::fs;
3use std::hash::Hash;
4use std::io;
5use std::panic::catch_unwind;
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8use thiserror::Error;
9
10use wasmer::{DeserializeError, Module, Target};
11
12use cosmwasm_std::Checksum;
13
14use crate::errors::{VmError, VmResult};
15use crate::filesystem::mkdir_p;
16use crate::modules::current_wasmer_module_version;
17use crate::wasm_backend::make_runtime_engine;
18use crate::wasm_backend::COST_FUNCTION_HASH;
19use crate::Size;
20
21use super::cached_module::engine_size_estimate;
22use super::CachedModule;
23
24const MODULE_SERIALIZATION_VERSION: &str = "v20";
71
72#[inline]
76fn raw_module_version_discriminator() -> String {
77 let hashes = [COST_FUNCTION_HASH];
78
79 let mut hasher = Blake2b::<U5>::new();
80
81 hasher.update(MODULE_SERIALIZATION_VERSION.as_bytes());
82 hasher.update(wasmer::VERSION.as_bytes());
83
84 for hash in hashes {
85 hasher.update(hash);
86 }
87
88 hex::encode(hasher.finalize())
89}
90
91#[inline]
110fn module_version_discriminator() -> &'static str {
111 static DISCRIMINATOR: OnceLock<String> = OnceLock::new();
112
113 DISCRIMINATOR.get_or_init(raw_module_version_discriminator)
114}
115
116pub struct FileSystemCache {
118 modules_path: PathBuf,
119 unchecked_modules: bool,
121}
122
123#[derive(Error, Debug)]
126pub enum NewFileSystemCacheError {
127 #[error("Could not get metadata of cache path")]
128 CouldntGetMetadata,
129 #[error("The supplied path is readonly")]
130 ReadonlyPath,
131 #[error("The supplied path already exists but is no directory")]
132 ExistsButNoDirectory,
133 #[error("Could not create cache path")]
134 CouldntCreatePath,
135}
136
137impl FileSystemCache {
138 pub unsafe fn new(
148 base_path: impl Into<PathBuf>,
149 unchecked_modules: bool,
150 ) -> Result<Self, NewFileSystemCacheError> {
151 let base_path: PathBuf = base_path.into();
152 if base_path.exists() {
153 let metadata = base_path
154 .metadata()
155 .map_err(|_e| NewFileSystemCacheError::CouldntGetMetadata)?;
156 if !metadata.is_dir() {
157 return Err(NewFileSystemCacheError::ExistsButNoDirectory);
158 }
159 if metadata.permissions().readonly() {
160 return Err(NewFileSystemCacheError::ReadonlyPath);
161 }
162 } else {
163 mkdir_p(&base_path).map_err(|_e| NewFileSystemCacheError::CouldntCreatePath)?;
165 }
166
167 Ok(Self {
168 modules_path: modules_path(
169 &base_path,
170 current_wasmer_module_version(),
171 &Target::default(),
172 ),
173 unchecked_modules,
174 })
175 }
176
177 pub fn set_module_unchecked(&mut self, unchecked: bool) {
180 self.unchecked_modules = unchecked;
181 }
182
183 fn module_file(&self, checksum: &Checksum) -> PathBuf {
185 let mut path = self.modules_path.clone();
186 path.push(checksum.to_hex());
187 path.set_extension("module");
188 path
189 }
190
191 pub fn load(
194 &self,
195 checksum: &Checksum,
196 memory_limit: Option<Size>,
197 ) -> VmResult<Option<CachedModule>> {
198 let file_path = self.module_file(checksum);
199
200 let engine = make_runtime_engine(memory_limit);
201 let result = if self.unchecked_modules {
202 unsafe { Module::deserialize_from_file_unchecked(&engine, &file_path) }
203 } else {
204 unsafe { Module::deserialize_from_file(&engine, &file_path) }
205 };
206 match result {
207 Ok(module) => {
208 let module_size = module_size(&file_path)?;
209 Ok(Some(CachedModule {
210 module,
211 engine,
212 size_estimate: module_size + engine_size_estimate(),
213 }))
214 }
215 Err(DeserializeError::Io(err)) => match err.kind() {
216 io::ErrorKind::NotFound => Ok(None),
217 _ => Err(VmError::cache_err(format!(
218 "Error opening module file: {err}"
219 ))),
220 },
221 Err(err) => Err(VmError::cache_err(format!(
222 "Error deserializing module: {err}"
223 ))),
224 }
225 }
226
227 pub fn store(&mut self, checksum: &Checksum, module: &Module) -> VmResult<usize> {
229 mkdir_p(&self.modules_path)
230 .map_err(|_e| VmError::cache_err("Error creating modules directory"))?;
231
232 let path = self.module_file(checksum);
233 catch_unwind(|| {
234 module
235 .serialize_to_file(&path)
236 .map_err(|e| VmError::cache_err(format!("Error writing module to disk: {e}")))
237 })
238 .map_err(|_| VmError::cache_err("Could not write module to disk"))??;
239 let module_size = module_size(&path)?;
240 Ok(module_size)
241 }
242
243 pub fn remove(&mut self, checksum: &Checksum) -> VmResult<bool> {
247 let file_path = self.module_file(checksum);
248
249 if file_path.exists() {
250 fs::remove_file(file_path)
251 .map_err(|_e| VmError::cache_err("Error deleting module from disk"))?;
252 Ok(true)
253 } else {
254 Ok(false)
255 }
256 }
257}
258
259fn module_size(module_path: &Path) -> VmResult<usize> {
261 let module_size: usize = module_path
262 .metadata()
263 .map_err(|_e| VmError::cache_err("Error getting file metadata"))? .len()
265 .try_into()
266 .expect("Could not convert file size to usize");
267 Ok(module_size)
268}
269
270fn target_id(target: &Target) -> String {
274 let mut deterministic_hasher = crc32fast::Hasher::new();
276 target.hash(&mut deterministic_hasher);
277 let hash = deterministic_hasher.finalize();
278 format!("{}-{:08X}", target.triple(), hash) }
280
281fn modules_path(base_path: &Path, wasmer_module_version: u32, target: &Target) -> PathBuf {
283 let version_dir = format!(
284 "{}-wasmer{wasmer_module_version}",
285 module_version_discriminator()
286 );
287 let target_dir = target_id(target);
288 base_path.join(version_dir).join(target_dir)
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::wasm_backend::{compile, make_compiling_engine};
295 use tempfile::TempDir;
296 use wasmer::{imports, Instance as WasmerInstance, Store};
297 use wasmer_middlewares::metering::set_remaining_points;
298
299 const TESTING_MEMORY_LIMIT: Option<Size> = Some(Size::mebi(16));
300 const TESTING_GAS_LIMIT: u64 = 500_000;
301
302 const SOME_WAT: &str = r#"(module
303 (type $t0 (func (param i32) (result i32)))
304 (func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)
305 local.get $p0
306 i32.const 1
307 i32.add))
308 "#;
309
310 #[test]
311 fn file_system_cache_run() {
312 let tmp_dir = TempDir::new().unwrap();
313 let mut cache = unsafe { FileSystemCache::new(tmp_dir.path(), false).unwrap() };
314
315 let wasm = wat::parse_str(SOME_WAT).unwrap();
317 let checksum = Checksum::generate(&wasm);
318
319 let cached = cache.load(&checksum, TESTING_MEMORY_LIMIT).unwrap();
321 assert!(cached.is_none());
322
323 let compiling_engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
325 let module = compile(&compiling_engine, &wasm).unwrap();
326 cache.store(&checksum, &module).unwrap();
327
328 let cached = cache.load(&checksum, TESTING_MEMORY_LIMIT).unwrap();
330 assert!(cached.is_some());
331
332 {
335 let CachedModule {
336 module: cached_module,
337 engine: runtime_engine,
338 size_estimate,
339 } = cached.unwrap();
340 assert_eq!(
341 size_estimate,
342 module.serialize().unwrap().len() + 10240 );
344 let import_object = imports! {};
345 let mut store = Store::new(runtime_engine);
346 let instance = WasmerInstance::new(&mut store, &cached_module, &import_object).unwrap();
347 set_remaining_points(&mut store, &instance, TESTING_GAS_LIMIT);
348 let add_one = instance.exports.get_function("add_one").unwrap();
349 let result = add_one.call(&mut store, &[42.into()]).unwrap();
350 assert_eq!(result[0].unwrap_i32(), 43);
351 }
352 }
353
354 #[test]
355 fn file_system_cache_store_uses_expected_path() {
356 let tmp_dir = TempDir::new().unwrap();
357 let mut cache = unsafe { FileSystemCache::new(tmp_dir.path(), false).unwrap() };
358
359 let wasm = wat::parse_str(SOME_WAT).unwrap();
361 let checksum = Checksum::generate(&wasm);
362
363 let engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
365 let module = compile(&engine, &wasm).unwrap();
366 cache.store(&checksum, &module).unwrap();
367
368 let discriminator = raw_module_version_discriminator();
369 let mut globber = glob::glob(&format!(
370 "{}/{}-wasmer8/**/{}.module",
371 tmp_dir.path().to_string_lossy(),
372 discriminator,
373 checksum
374 ))
375 .expect("Failed to read glob pattern");
376 let file_path = globber.next().unwrap().unwrap();
377 let _serialized_module = fs::read(file_path).unwrap();
378 }
379
380 #[test]
381 fn file_system_cache_remove_works() {
382 let tmp_dir = TempDir::new().unwrap();
383 let mut cache = unsafe { FileSystemCache::new(tmp_dir.path(), false).unwrap() };
384
385 let wasm = wat::parse_str(SOME_WAT).unwrap();
387 let checksum = Checksum::generate(&wasm);
388
389 let compiling_engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
391 let module = compile(&compiling_engine, &wasm).unwrap();
392 cache.store(&checksum, &module).unwrap();
393
394 assert!(cache
396 .load(&checksum, TESTING_MEMORY_LIMIT)
397 .unwrap()
398 .is_some());
399
400 let existed = cache.remove(&checksum).unwrap();
402 assert!(existed);
403
404 assert!(cache
406 .load(&checksum, TESTING_MEMORY_LIMIT)
407 .unwrap()
408 .is_none());
409
410 let existed = cache.remove(&checksum).unwrap();
412 assert!(!existed);
413 }
414
415 #[test]
416 fn target_id_works() {
417 let triple = wasmer::Triple {
418 architecture: wasmer::Architecture::X86_64,
419 vendor: target_lexicon::Vendor::Nintendo,
420 operating_system: target_lexicon::OperatingSystem::Fuchsia,
421 environment: target_lexicon::Environment::Gnu,
422 binary_format: target_lexicon::BinaryFormat::Coff,
423 };
424 let target = Target::new(triple.clone(), wasmer::CpuFeature::POPCNT.into());
425 let id = target_id(&target);
426 assert_eq!(id, "x86_64-nintendo-fuchsia-gnu-coff-719EEF18");
427 let target = Target::new(triple, wasmer::CpuFeature::AVX512DQ.into());
429 let id = target_id(&target);
430 assert_eq!(id, "x86_64-nintendo-fuchsia-gnu-coff-E3770FA3");
431
432 let target = Target::default();
434 let id1 = target_id(&target);
435 let id2 = target_id(&target);
436 assert_eq!(id1, id2);
437 }
438
439 #[test]
440 fn modules_path_works() {
441 let base = PathBuf::from("modules");
442 let triple = wasmer::Triple {
443 architecture: wasmer::Architecture::X86_64,
444 vendor: target_lexicon::Vendor::Nintendo,
445 operating_system: target_lexicon::OperatingSystem::Fuchsia,
446 environment: target_lexicon::Environment::Gnu,
447 binary_format: target_lexicon::BinaryFormat::Coff,
448 };
449 let target = Target::new(triple, wasmer::CpuFeature::POPCNT.into());
450 let p = modules_path(&base, 17, &target);
451 let discriminator = raw_module_version_discriminator();
452
453 assert_eq!(
454 p.as_os_str(),
455 if cfg!(windows) {
456 format!(
457 "modules\\{discriminator}-wasmer17\\x86_64-nintendo-fuchsia-gnu-coff-719EEF18"
458 )
459 } else {
460 format!(
461 "modules/{discriminator}-wasmer17/x86_64-nintendo-fuchsia-gnu-coff-719EEF18"
462 )
463 }
464 .as_str()
465 );
466 }
467
468 #[test]
469 fn module_version_discriminator_stays_the_same() {
470 let v1 = raw_module_version_discriminator();
471 let v2 = raw_module_version_discriminator();
472 let v3 = raw_module_version_discriminator();
473 let v4 = raw_module_version_discriminator();
474
475 assert_eq!(v1, v2);
476 assert_eq!(v2, v3);
477 assert_eq!(v3, v4);
478 }
479
480 #[test]
481 fn module_version_static() {
482 let version = raw_module_version_discriminator();
483 assert_eq!(version, "6c36aacf76");
484 }
485}