wasmer_wasix/runners/
wasi_common.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use anyhow::{Context, Error};
8use futures::future::BoxFuture;
9use tokio::runtime::Handle;
10use virtual_fs::{FileSystem, FsError, OverlayFileSystem, RootFileSystemBuilder, TmpFileSystem};
11use webc::metadata::annotations::Wasi as WasiAnnotation;
12
13use crate::{
14    WasiEnvBuilder,
15    bin_factory::BinaryPackage,
16    capabilities::Capabilities,
17    journal::{DynJournal, DynReadableJournal, SnapshotTrigger},
18};
19
20pub const MAPPED_CURRENT_DIR_DEFAULT_PATH: &str = "/home";
21
22#[derive(Debug, Clone)]
23pub struct MappedCommand {
24    /// The new alias.
25    pub alias: String,
26    /// The original command.
27    pub target: String,
28}
29
30#[derive(Debug, Default, Clone)]
31pub(crate) struct CommonWasiOptions {
32    pub(crate) entry_function: Option<String>,
33    pub(crate) args: Vec<String>,
34    pub(crate) env: HashMap<String, String>,
35    pub(crate) forward_host_env: bool,
36    pub(crate) mapped_host_commands: Vec<MappedCommand>,
37    pub(crate) mounts: Vec<MountedDirectory>,
38    pub(crate) is_home_mapped: bool,
39    pub(crate) injected_packages: Vec<BinaryPackage>,
40    pub(crate) capabilities: Capabilities,
41    pub(crate) read_only_journals: Vec<Arc<DynReadableJournal>>,
42    pub(crate) writable_journals: Vec<Arc<DynJournal>>,
43    pub(crate) snapshot_on: Vec<SnapshotTrigger>,
44    pub(crate) snapshot_interval: Option<std::time::Duration>,
45    pub(crate) stop_running_after_snapshot: bool,
46    pub(crate) skip_stdio_during_bootstrap: bool,
47    pub(crate) current_dir: Option<PathBuf>,
48}
49
50impl CommonWasiOptions {
51    pub(crate) fn prepare_webc_env(
52        &self,
53        builder: &mut WasiEnvBuilder,
54        container_fs: Option<Arc<dyn FileSystem + Send + Sync>>,
55        wasi: &WasiAnnotation,
56        root_fs: Option<TmpFileSystem>,
57    ) -> Result<(), anyhow::Error> {
58        if let Some(ref entry_function) = self.entry_function {
59            builder.set_entry_function(entry_function);
60        }
61
62        let root_fs = root_fs.unwrap_or_else(|| {
63            let mapped_dirs = self
64                .mounts
65                .iter()
66                .map(|d| d.guest.as_str())
67                .collect::<Vec<_>>();
68            RootFileSystemBuilder::default().build_ext(&mapped_dirs)
69        });
70        let fs = prepare_filesystem(root_fs, &self.mounts, container_fs)?;
71
72        // TODO: What's a preopen for '.' supposed to mean anyway? Why do we need it?
73        if self.mounts.iter().all(|m| m.guest != ".") {
74            // The user hasn't mounted "." to anything, so let's map it to "/"
75            let path = builder.get_current_dir().unwrap_or(PathBuf::from("/"));
76            builder.add_map_dir(".", path)?;
77        }
78
79        builder.add_preopen_dir("/")?;
80
81        builder.set_fs(Box::new(fs));
82
83        for pkg in &self.injected_packages {
84            builder.add_webc(pkg.clone());
85        }
86
87        let mapped_cmds = self
88            .mapped_host_commands
89            .iter()
90            .map(|c| (c.alias.as_str(), c.target.as_str()));
91        builder.add_mapped_commands(mapped_cmds);
92
93        self.populate_env(wasi, builder);
94        self.populate_args(wasi, builder);
95
96        *builder.capabilities_mut() = self.capabilities.clone();
97
98        #[cfg(feature = "journal")]
99        {
100            for journal in &self.read_only_journals {
101                builder.add_read_only_journal(journal.clone());
102            }
103            for journal in &self.writable_journals {
104                builder.add_writable_journal(journal.clone());
105            }
106            for trigger in &self.snapshot_on {
107                builder.add_snapshot_trigger(*trigger);
108            }
109            if let Some(interval) = self.snapshot_interval {
110                builder.with_snapshot_interval(interval);
111            }
112            builder.with_stop_running_after_snapshot(self.stop_running_after_snapshot);
113        }
114
115        Ok(())
116    }
117
118    fn populate_env(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) {
119        for item in wasi.env.as_deref().unwrap_or_default() {
120            // TODO(Michael-F-Bryan): Convert "wasi.env" in the webc crate from an
121            // Option<Vec<String>> to a HashMap<String, String> so we avoid this
122            // string.split() business
123            match item.split_once('=') {
124                Some((k, v)) => {
125                    builder.add_env(k, v);
126                }
127                None => {
128                    builder.add_env(item, String::new());
129                }
130            }
131        }
132
133        if self.forward_host_env {
134            builder.add_envs(std::env::vars());
135        }
136
137        builder.add_envs(self.env.clone());
138    }
139
140    fn populate_args(&self, wasi: &WasiAnnotation, builder: &mut WasiEnvBuilder) {
141        if let Some(main_args) = &wasi.main_args {
142            builder.add_args(main_args);
143        }
144
145        builder.add_args(&self.args);
146    }
147}
148
149// type ContainerFs =
150//     OverlayFileSystem<TmpFileSystem, [RelativeOrAbsolutePathHack<Arc<dyn FileSystem>>; 1]>;
151
152fn build_directory_mappings(
153    root_fs: &mut TmpFileSystem,
154    mounted_dirs: &[MountedDirectory],
155) -> Result<(), anyhow::Error> {
156    for dir in mounted_dirs {
157        let MountedDirectory {
158            guest: guest_path,
159            fs,
160        } = dir;
161        let mut guest_path = PathBuf::from(guest_path);
162        tracing::debug!(
163            guest=%guest_path.display(),
164            "Mounting",
165        );
166
167        if guest_path.is_relative() {
168            guest_path = apply_relative_path_mounting_hack(&guest_path);
169        }
170
171        let guest_path = root_fs
172            .canonicalize_unchecked(&guest_path)
173            .with_context(|| {
174                format!(
175                    "Unable to canonicalize guest path '{}'",
176                    guest_path.display()
177                )
178            })?;
179
180        if guest_path == Path::new("/") {
181            root_fs
182                .mount_directory_entries(&guest_path, fs, "/".as_ref())
183                .context("Unable to mount to root")?;
184        } else {
185            if let Some(parent) = guest_path.parent() {
186                create_dir_all(&*root_fs, parent).with_context(|| {
187                    format!("Unable to create the \"{}\" directory", parent.display())
188                })?;
189            }
190
191            TmpFileSystem::mount(root_fs, guest_path.clone(), fs, "/".into())
192                .with_context(|| format!("Unable to mount \"{}\"", guest_path.display()))?;
193        }
194    }
195
196    Ok(())
197}
198
199fn prepare_filesystem(
200    mut root_fs: TmpFileSystem,
201    mounted_dirs: &[MountedDirectory],
202    container_fs: Option<Arc<dyn FileSystem + Send + Sync>>,
203) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
204    if !mounted_dirs.is_empty() {
205        build_directory_mappings(&mut root_fs, mounted_dirs)?;
206    }
207
208    // HACK(Michael-F-Bryan): The WebcVolumeFileSystem only accepts relative
209    // paths, but our Python executable will try to access its standard library
210    // with relative paths assuming that it is being run from the root
211    // directory (i.e. it does `open("lib/python3.6/io.py")` instead of
212    // `open("/lib/python3.6/io.py")`).
213    // Until the FileSystem trait figures out whether relative paths should be
214    // supported or not, we'll add an adapter that automatically retries
215    // operations using an absolute path if it failed using a relative path.
216
217    let fs = if let Some(container) = container_fs {
218        let container = RelativeOrAbsolutePathHack(container);
219        let fs = OverlayFileSystem::new(root_fs, [container]);
220        Box::new(fs) as Box<dyn FileSystem + Send + Sync>
221    } else {
222        let fs = RelativeOrAbsolutePathHack(root_fs);
223        Box::new(fs) as Box<dyn FileSystem + Send + Sync>
224    };
225
226    Ok(fs)
227}
228
229/// HACK: We need this so users can mount host directories at relative paths.
230/// This assumes that the current directory when a runner starts will be "/", so
231/// instead of mounting to a relative path, we just mount to "/$path".
232///
233/// This isn't really a long-term solution because there is no guarantee what
234/// the current directory will be. The WASI spec also doesn't require the
235/// current directory to be part of the "main" filesystem at all, we really
236/// *should* be mounting to a relative directory but that isn't supported by our
237/// virtual fs layer.
238///
239/// See <https://github.com/wasmerio/wasmer/issues/3794> for more.
240fn apply_relative_path_mounting_hack(original: &Path) -> PathBuf {
241    debug_assert!(original.is_relative());
242
243    let root = Path::new("/");
244    let mapped_path = if original == Path::new(".") {
245        root.to_path_buf()
246    } else {
247        root.join(original)
248    };
249
250    tracing::debug!(
251        original_path=%original.display(),
252        remapped_path=%mapped_path.display(),
253        "Remapping a relative path"
254    );
255
256    mapped_path
257}
258
259fn create_dir_all(fs: &dyn FileSystem, path: &Path) -> Result<(), Error> {
260    if fs.metadata(path).is_ok() {
261        return Ok(());
262    }
263
264    if let Some(parent) = path.parent() {
265        create_dir_all(fs, parent)?;
266    }
267
268    fs.create_dir(path)?;
269
270    Ok(())
271}
272
273#[derive(Debug, Clone)]
274pub struct MountedDirectory {
275    pub guest: String,
276    pub fs: Arc<dyn FileSystem + Send + Sync>,
277}
278
279/// A directory that should be mapped from the host filesystem into a WASI
280/// instance (the "guest").
281///
282/// # Panics
283///
284/// Converting a [`MappedDirectory`] to a [`MountedDirectory`] requires enabling
285/// the `host-fs` feature flag. Using the [`From`] implementation without
286/// enabling this feature will result in a runtime panic.
287#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
288pub struct MappedDirectory {
289    /// The absolute path for a directory on the host filesystem.
290    pub host: std::path::PathBuf,
291    /// The absolute path specifying where the host directory should be mounted
292    /// inside the guest.
293    pub guest: String,
294}
295
296impl From<MappedDirectory> for MountedDirectory {
297    fn from(value: MappedDirectory) -> Self {
298        cfg_if::cfg_if! {
299            if #[cfg(feature = "host-fs")] {
300                let MappedDirectory { host, guest } = value;
301                let fs: Arc<dyn FileSystem + Send + Sync> =
302                    Arc::new(virtual_fs::host_fs::FileSystem::new(Handle::current(), host).unwrap());
303
304                MountedDirectory { guest, fs }
305            } else {
306                unreachable!("The `host-fs` feature needs to be enabled to map {value:?}")
307            }
308        }
309    }
310}
311
312#[derive(Debug)]
313struct RelativeOrAbsolutePathHack<F>(F);
314
315impl<F: FileSystem> RelativeOrAbsolutePathHack<F> {
316    fn execute<Func, Ret>(&self, path: &Path, operation: Func) -> Result<Ret, FsError>
317    where
318        Func: Fn(&F, &Path) -> Result<Ret, FsError>,
319    {
320        // First, try it with the path we were given
321        let result = operation(&self.0, path);
322
323        if result.is_err() && !path.is_absolute() {
324            // we were given a relative path, but maybe the operation will work
325            // using absolute paths instead.
326            let path = Path::new("/").join(path);
327            operation(&self.0, &path)
328        } else {
329            result
330        }
331    }
332}
333
334impl<F: FileSystem> virtual_fs::FileSystem for RelativeOrAbsolutePathHack<F> {
335    fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
336        self.execute(path, |fs, p| fs.readlink(p))
337    }
338
339    fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
340        self.execute(path, |fs, p| fs.read_dir(p))
341    }
342
343    fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
344        self.execute(path, |fs, p| fs.create_dir(p))
345    }
346
347    fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
348        self.execute(path, |fs, p| fs.remove_dir(p))
349    }
350
351    fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
352        let from = from.to_owned();
353        let to = to.to_owned();
354        Box::pin(async move { self.0.rename(&from, &to).await })
355    }
356
357    fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
358        self.execute(path, |fs, p| fs.metadata(p))
359    }
360
361    fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
362        self.execute(path, |fs, p| fs.symlink_metadata(p))
363    }
364
365    fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
366        self.execute(path, |fs, p| fs.remove_file(p))
367    }
368
369    fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
370        virtual_fs::OpenOptions::new(self)
371    }
372
373    fn mount(
374        &self,
375        name: String,
376        path: &Path,
377        fs: Box<dyn FileSystem + Send + Sync>,
378    ) -> virtual_fs::Result<()> {
379        let name_ref = &name;
380        let f_ref = &Arc::new(fs);
381        self.execute(path, move |f, p| {
382            f.mount(name_ref.clone(), p, Box::new(f_ref.clone()))
383        })
384    }
385}
386
387impl<F: FileSystem> virtual_fs::FileOpener for RelativeOrAbsolutePathHack<F> {
388    fn open(
389        &self,
390        path: &Path,
391        conf: &virtual_fs::OpenOptionsConfig,
392    ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
393        self.execute(path, |fs, p| {
394            fs.new_open_options().options(conf.clone()).open(p)
395        })
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use std::time::SystemTime;
402
403    use tempfile::TempDir;
404    use virtual_fs::{DirEntry, FileType, Metadata, WebcVolumeFileSystem};
405    use wasmer_package::utils::from_bytes;
406
407    use super::*;
408
409    const PYTHON: &[u8] = include_bytes!("../../../c-api/examples/assets/python-0.1.0.wasmer");
410
411    /// Fixes <https://github.com/wasmerio/wasmer/issues/3789>
412    #[tokio::test]
413    async fn mix_args_from_the_webc_and_user() {
414        let args = CommonWasiOptions {
415            args: vec!["extra".to_string(), "args".to_string()],
416            ..Default::default()
417        };
418        let mut builder = WasiEnvBuilder::new("program-name");
419        let fs = Arc::new(virtual_fs::EmptyFileSystem::default());
420        let mut annotations = WasiAnnotation::new("some-atom");
421        annotations.main_args = Some(vec![
422            "hard".to_string(),
423            "coded".to_string(),
424            "args".to_string(),
425        ]);
426
427        args.prepare_webc_env(&mut builder, Some(fs), &annotations, None)
428            .unwrap();
429
430        assert_eq!(
431            builder.get_args(),
432            [
433                // the program name from
434                "program-name",
435                // from the WEBC's annotations
436                "hard",
437                "coded",
438                "args",
439                // from the user
440                "extra",
441                "args",
442            ]
443        );
444    }
445
446    #[tokio::test]
447    async fn mix_env_vars_from_the_webc_and_user() {
448        let args = CommonWasiOptions {
449            env: vec![("EXTRA".to_string(), "envs".to_string())]
450                .into_iter()
451                .collect(),
452            ..Default::default()
453        };
454        let mut builder = WasiEnvBuilder::new("python");
455        let fs = Arc::new(virtual_fs::EmptyFileSystem::default());
456        let mut annotations = WasiAnnotation::new("python");
457        annotations.env = Some(vec!["HARD_CODED=env-vars".to_string()]);
458
459        args.prepare_webc_env(&mut builder, Some(fs), &annotations, None)
460            .unwrap();
461
462        assert_eq!(
463            builder.get_env(),
464            [
465                ("HARD_CODED".to_string(), b"env-vars".to_vec()),
466                ("EXTRA".to_string(), b"envs".to_vec()),
467            ]
468        );
469    }
470
471    #[tokio::test]
472    #[cfg_attr(not(feature = "host-fs"), ignore)]
473    async fn python_use_case() {
474        let temp = TempDir::new().unwrap();
475        let sub_dir = temp.path().join("path").join("to");
476        std::fs::create_dir_all(&sub_dir).unwrap();
477        std::fs::write(sub_dir.join("file.txt"), b"Hello, World!").unwrap();
478        let mapping = [MountedDirectory::from(MappedDirectory {
479            guest: "/home".to_string(),
480            host: sub_dir,
481        })];
482        let container = from_bytes(PYTHON).unwrap();
483        let webc_fs = WebcVolumeFileSystem::mount_all(&container);
484
485        let root_fs = RootFileSystemBuilder::default().build();
486        let fs = prepare_filesystem(root_fs, &mapping, Some(Arc::new(webc_fs))).unwrap();
487
488        assert!(fs.metadata("/home/file.txt".as_ref()).unwrap().is_file());
489        assert!(fs.metadata("lib".as_ref()).unwrap().is_dir());
490        assert!(
491            fs.metadata("lib/python3.6/collections/__init__.py".as_ref())
492                .unwrap()
493                .is_file()
494        );
495        assert!(
496            fs.metadata("lib/python3.6/encodings/__init__.py".as_ref())
497                .unwrap()
498                .is_file()
499        );
500    }
501
502    fn unix_timestamp_nanos(instant: SystemTime) -> Option<u64> {
503        let duration = instant.duration_since(SystemTime::UNIX_EPOCH).ok()?;
504        Some(duration.as_nanos() as u64)
505    }
506
507    #[tokio::test]
508    #[cfg_attr(not(feature = "host-fs"), ignore)]
509    async fn convert_mapped_directory_to_mounted_directory() {
510        let temp = TempDir::new().unwrap();
511        let dir = MappedDirectory {
512            guest: "/mnt/dir".to_string(),
513            host: temp.path().to_path_buf(),
514        };
515        let contents = "Hello, World!";
516        let file_txt = temp.path().join("file.txt");
517        std::fs::write(&file_txt, contents).unwrap();
518        let metadata = std::fs::metadata(&file_txt).unwrap();
519
520        let got = MountedDirectory::from(dir);
521
522        let directory_contents: Vec<_> = got
523            .fs
524            .read_dir("/".as_ref())
525            .unwrap()
526            .map(|entry| entry.unwrap())
527            .collect();
528        assert_eq!(
529            directory_contents,
530            vec![DirEntry {
531                path: PathBuf::from("/file.txt"),
532                metadata: Ok(Metadata {
533                    ft: FileType::new_file(),
534                    // Note: Some timestamps aren't available on MUSL and will
535                    // default to zero.
536                    accessed: metadata
537                        .accessed()
538                        .ok()
539                        .and_then(unix_timestamp_nanos)
540                        .unwrap_or(0),
541                    created: metadata
542                        .created()
543                        .ok()
544                        .and_then(unix_timestamp_nanos)
545                        .unwrap_or(0),
546                    modified: metadata
547                        .modified()
548                        .ok()
549                        .and_then(unix_timestamp_nanos)
550                        .unwrap_or(0),
551                    len: contents.len() as u64,
552                })
553            }]
554        );
555    }
556}