1mod fd;
18mod fd_list;
19mod inode_guard;
20mod notification;
21mod path_posix;
22
23use std::{
24 borrow::Cow,
25 collections::{HashMap, HashSet},
26 ops::{Deref, DerefMut},
27 path::{Path, PathBuf},
28 pin::Pin,
29 sync::{
30 Arc, Mutex, RwLock, Weak,
31 atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering},
32 },
33 task::{Context, Poll},
34};
35
36use crate::{
37 net::socket::InodeSocketKind,
38 state::{Stderr, Stdin, Stdout},
39};
40use futures::{Future, future::BoxFuture};
41use tracing::{debug, trace, warn};
42use virtual_fs::{
43 ArcFileSystem, FileSystem, FsError, MountFileSystem, OpenOptions, OverlayFileSystem,
44 TmpFileSystem, VirtualFile, limiter::DynFsMemoryLimiter,
45};
46use wasmer_config::package::PackageId;
47use wasmer_wasix_types::{
48 types::{__WASI_STDERR_FILENO, __WASI_STDIN_FILENO, __WASI_STDOUT_FILENO},
49 wasi::{
50 Errno, Fd as WasiFd, Fdflags, Fdflagsext, Fdstat, Filesize, Filestat, Filetype,
51 Preopentype, Prestat, PrestatEnum, Rights, Socktype,
52 },
53};
54
55pub(crate) use self::fd::VirtualFileLock;
56pub use self::fd::{Fd, FdInner, InodeVal, Kind, SymlinkKind};
57pub(crate) use self::fd_list::FdList;
58pub(crate) use self::inode_guard::{
59 InodeValFilePollGuard, InodeValFilePollGuardJoin, InodeValFilePollGuardMode,
60 InodeValFileReadGuard, InodeValFileWriteGuard, WasiStateFileGuard,
61};
62pub use self::notification::NotificationInner;
63pub(crate) use self::path_posix::{PosixPath, PosixPathBuf, PosixPathComponent};
64use crate::{ALL_RIGHTS, bin_factory::BinaryPackage, state::PreopenedDir};
65
66pub(crate) const MAX_FD: WasiFd = (64 * 1024) - 1;
70
71pub(crate) struct FlushPoller {
72 pub(crate) file: VirtualFileLock,
73}
74
75pub(crate) struct CloseFdOutcome {
78 pub skipped_preopen: bool,
79 pub removed: bool,
80 pub flush_target: Option<VirtualFileLock>,
81}
82
83impl CloseFdOutcome {
84 fn not_found() -> Self {
85 Self {
86 skipped_preopen: false,
87 removed: false,
88 flush_target: None,
89 }
90 }
91}
92
93impl Future for FlushPoller {
94 type Output = Result<(), Errno>;
95
96 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
97 let mut file = self.file.write().unwrap();
98 Pin::new(file.as_mut())
99 .poll_flush(cx)
100 .map_err(|_| Errno::Io)
101 }
102}
103
104pub const VIRTUAL_ROOT_FD: WasiFd = 3;
118
119pub const FS_STDIN_INO: Inode = Inode(10);
122pub const FS_STDOUT_INO: Inode = Inode(11);
123pub const FS_STDERR_INO: Inode = Inode(12);
124pub const FS_ROOT_INO: Inode = Inode(13);
125
126const STDIN_DEFAULT_RIGHTS: Rights = {
127 Rights::from_bits_truncate(
130 Rights::FD_DATASYNC.bits()
131 | Rights::FD_READ.bits()
132 | Rights::FD_SYNC.bits()
133 | Rights::FD_ADVISE.bits()
134 | Rights::FD_FILESTAT_GET.bits()
135 | Rights::FD_FDSTAT_SET_FLAGS.bits()
136 | Rights::POLL_FD_READWRITE.bits(),
137 )
138};
139const STDOUT_DEFAULT_RIGHTS: Rights = {
140 Rights::from_bits_truncate(
143 Rights::FD_DATASYNC.bits()
144 | Rights::FD_SYNC.bits()
145 | Rights::FD_WRITE.bits()
146 | Rights::FD_ADVISE.bits()
147 | Rights::FD_FILESTAT_GET.bits()
148 | Rights::FD_FDSTAT_SET_FLAGS.bits()
149 | Rights::POLL_FD_READWRITE.bits(),
150 )
151};
152const STDERR_DEFAULT_RIGHTS: Rights = STDOUT_DEFAULT_RIGHTS;
153
154pub const MAX_SYMLINKS: u32 = 128;
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
159pub struct Inode(u64);
160
161impl Inode {
162 pub fn as_u64(&self) -> u64 {
163 self.0
164 }
165
166 pub fn from_path(str: &str) -> Self {
167 Inode(xxhash_rust::xxh64::xxh64(str.as_bytes(), 0))
168 }
169}
170
171#[derive(Debug, Clone)]
172pub struct InodeGuard {
173 ino: Inode,
174 inner: Arc<InodeVal>,
175
176 open_handles: Arc<AtomicI32>,
181}
182impl InodeGuard {
183 pub fn ino(&self) -> Inode {
184 self.ino
185 }
186
187 pub fn downgrade(&self) -> InodeWeakGuard {
188 InodeWeakGuard {
189 ino: self.ino,
190 open_handles: self.open_handles.clone(),
191 inner: Arc::downgrade(&self.inner),
192 }
193 }
194
195 pub fn ref_cnt(&self) -> usize {
196 Arc::strong_count(&self.inner)
197 }
198
199 pub fn handle_count(&self) -> u32 {
200 self.open_handles.load(Ordering::SeqCst) as u32
201 }
202
203 pub fn acquire_handle(&self) {
204 let prev_handles = self.open_handles.fetch_add(1, Ordering::SeqCst);
205 trace!(ino = %self.ino.0, new_count = %(prev_handles + 1), "acquiring handle for InodeGuard");
206 }
207
208 pub fn drop_one_handle(&self) {
209 let prev_handles = self.open_handles.fetch_sub(1, Ordering::SeqCst);
210
211 trace!(ino = %self.ino.0, %prev_handles, "dropping handle for InodeGuard");
212
213 if prev_handles > 1 {
215 return;
216 }
217
218 let mut guard = self.inner.write();
220
221 if prev_handles != 1 {
226 panic!("InodeGuard handle dropped too many times");
227 }
228
229 if self.open_handles.load(Ordering::SeqCst) != 0 {
231 return;
232 }
233
234 let ino = self.ino.0;
235 trace!(%ino, "InodeGuard has no more open handles");
236
237 match guard.deref_mut() {
238 Kind::File { handle, .. } if handle.is_some() => {
239 let file_ref_count = Arc::strong_count(handle.as_ref().unwrap());
240 trace!(%file_ref_count, %ino, "dropping file handle");
241 drop(handle.take().unwrap());
242 }
243 Kind::PipeRx { rx } => {
244 trace!(%ino, "closing pipe rx");
245 rx.close();
246 }
247 Kind::PipeTx { tx } => {
248 trace!(%ino, "closing pipe tx");
249 tx.close();
250 }
251 _ => (),
252 }
253 }
254}
255impl std::ops::Deref for InodeGuard {
256 type Target = InodeVal;
257 fn deref(&self) -> &Self::Target {
258 self.inner.deref()
259 }
260}
261
262#[derive(Debug, Clone)]
263pub struct InodeWeakGuard {
264 ino: Inode,
265 open_handles: Arc<AtomicI32>,
269 inner: Weak<InodeVal>,
270}
271impl InodeWeakGuard {
272 pub fn ino(&self) -> Inode {
273 self.ino
274 }
275 pub fn upgrade(&self) -> Option<InodeGuard> {
276 Weak::upgrade(&self.inner).map(|inner| InodeGuard {
277 ino: self.ino,
278 open_handles: self.open_handles.clone(),
279 inner,
280 })
281 }
282}
283
284#[derive(Debug)]
285struct EphemeralSymlinkEntry {
286 path_to_symlink: PathBuf,
287 relative_path: PathBuf,
288}
289
290#[derive(Debug)]
291#[warn(unused)]
292enum ComponentResolution {
293 Create {
294 kind: Kind,
295 name: String,
296 entry_name: String,
297 is_ephemeral: bool,
298 },
299 BackingSymlink {
300 file: PathBuf,
301 link_value: PathBuf,
302 entry_name: String,
303 },
304 #[cfg(unix)]
305 Special {
306 kind: Kind,
307 name: Cow<'static, str>,
308 entry_name: String,
309 stat: Filestat,
310 },
311}
312
313#[derive(Debug)]
314struct WasiInodesProtected {
315 lookup: HashMap<Inode, Weak<InodeVal>>,
316}
317
318#[derive(Clone, Debug)]
319pub struct WasiInodes {
320 protected: Arc<RwLock<WasiInodesProtected>>,
321}
322
323impl WasiInodes {
324 pub fn new() -> Self {
325 Self {
326 protected: Arc::new(RwLock::new(WasiInodesProtected {
327 lookup: Default::default(),
328 })),
329 }
330 }
331
332 pub fn add_inode_val(&self, val: InodeVal) -> InodeGuard {
334 let val = Arc::new(val);
335 let st_ino = {
336 let guard = val.stat.read().unwrap();
337 guard.st_ino
338 };
339
340 let mut guard = self.protected.write().unwrap();
341 let ino = Inode(st_ino);
342 guard.lookup.insert(ino, Arc::downgrade(&val));
343
344 if guard.lookup.len() % 100 == 1 {
346 guard.lookup.retain(|_, v| Weak::strong_count(v) > 0);
347 }
348
349 let open_handles = Arc::new(AtomicI32::new(0));
350
351 InodeGuard {
352 ino,
353 open_handles,
354 inner: val,
355 }
356 }
357
358 pub(crate) fn stdout_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
360 Self::std_dev_get_mut(fd_map, __WASI_STDOUT_FILENO)
361 }
362
363 pub(crate) fn stderr_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
365 Self::std_dev_get_mut(fd_map, __WASI_STDERR_FILENO)
366 }
367
368 #[allow(dead_code)]
371 pub(crate) fn stdin(fd_map: &RwLock<FdList>) -> Result<InodeValFileReadGuard, FsError> {
372 Self::std_dev_get(fd_map, __WASI_STDIN_FILENO)
373 }
374 pub(crate) fn stdin_mut(fd_map: &RwLock<FdList>) -> Result<InodeValFileWriteGuard, FsError> {
376 Self::std_dev_get_mut(fd_map, __WASI_STDIN_FILENO)
377 }
378
379 fn std_dev_get(fd_map: &RwLock<FdList>, fd: WasiFd) -> Result<InodeValFileReadGuard, FsError> {
382 if let Some(fd) = fd_map.read().unwrap().get(fd) {
383 let guard = fd.inode.read();
384 if let Kind::File {
385 handle: Some(handle),
386 ..
387 } = guard.deref()
388 {
389 Ok(InodeValFileReadGuard::new(handle))
390 } else {
391 Err(FsError::NotAFile)
393 }
394 } else {
395 Err(FsError::NoDevice)
397 }
398 }
399 fn std_dev_get_mut(
402 fd_map: &RwLock<FdList>,
403 fd: WasiFd,
404 ) -> Result<InodeValFileWriteGuard, FsError> {
405 if let Some(fd) = fd_map.read().unwrap().get(fd) {
406 let guard = fd.inode.read();
407 if let Kind::File {
408 handle: Some(handle),
409 ..
410 } = guard.deref()
411 {
412 Ok(InodeValFileWriteGuard::new(handle))
413 } else {
414 Err(FsError::NotAFile)
416 }
417 } else {
418 Err(FsError::NoDevice)
420 }
421 }
422}
423
424impl Default for WasiInodes {
425 fn default() -> Self {
426 Self::new()
427 }
428}
429
430#[derive(Debug, Clone)]
431pub struct WasiFsRoot {
432 root: Arc<MountFileSystem>,
433 memory_limiter: Option<DynFsMemoryLimiter>,
434}
435
436impl WasiFsRoot {
437 pub fn from_mount_fs(root: MountFileSystem) -> Self {
438 Self {
439 root: Arc::new(root),
440 memory_limiter: None,
441 }
442 }
443
444 pub fn from_filesystem(fs: Arc<dyn FileSystem + Send + Sync>) -> Self {
445 let root = MountFileSystem::new();
446 root.mount(Path::new("/"), fs)
447 .expect("mounting the root fs on an empty mount fs should succeed");
448
449 Self {
450 root: Arc::new(root),
451 memory_limiter: None,
452 }
453 }
454
455 pub fn with_memory_limiter_opt(mut self, limiter: Option<DynFsMemoryLimiter>) -> Self {
456 self.memory_limiter = limiter;
457 self
458 }
459
460 pub(crate) fn memory_limiter(&self) -> Option<&DynFsMemoryLimiter> {
461 self.memory_limiter.as_ref()
462 }
463
464 pub(crate) fn root(&self) -> &Arc<MountFileSystem> {
465 &self.root
466 }
467
468 pub(crate) fn writable_root(&self) -> Option<TmpFileSystem> {
469 let root = self.root.filesystem_at(Path::new("/"))?;
470 find_writable_root(root.as_ref())
471 }
472
473 pub(crate) fn stack_root_filesystem(
474 &self,
475 lower: Arc<dyn FileSystem + Send + Sync>,
476 ) -> Result<(), FsError> {
477 let current = self
478 .root
479 .filesystem_at(Path::new("/"))
480 .ok_or(FsError::EntryNotFound)?;
481 let overlay =
482 OverlayFileSystem::new(ArcFileSystem::new(current), [ArcFileSystem::new(lower)]);
483 self.root.set_mount(Path::new("/"), Arc::new(overlay))
484 }
485}
486
487fn find_writable_root(fs: &(dyn FileSystem + Send + Sync)) -> Option<TmpFileSystem> {
488 if let Some(tmp) = fs.upcast_any_ref().downcast_ref::<TmpFileSystem>() {
489 return Some(tmp.clone());
490 }
491
492 if let Some(arc_fs) = fs.upcast_any_ref().downcast_ref::<ArcFileSystem>() {
493 return find_writable_root(arc_fs.inner().as_ref());
494 }
495
496 if let Some(overlay) = fs
497 .upcast_any_ref()
498 .downcast_ref::<OverlayFileSystem<ArcFileSystem, Vec<Arc<dyn FileSystem + Send + Sync>>>>()
499 {
500 return find_writable_root(overlay.primary());
501 }
502
503 if let Some(overlay) = fs
504 .upcast_any_ref()
505 .downcast_ref::<OverlayFileSystem<ArcFileSystem, [ArcFileSystem; 1]>>()
506 {
507 return find_writable_root(overlay.primary());
508 }
509
510 None
511}
512
513impl FileSystem for WasiFsRoot {
514 fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
515 self.root.readlink(path)
516 }
517
518 fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
519 self.root.read_dir(path)
520 }
521
522 fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
523 self.root.create_dir(path)
524 }
525
526 fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
527 self.root.create_symlink(source, target)
528 }
529
530 fn hard_link(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> {
531 self.root.hard_link(source, target)
532 }
533
534 fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
535 self.root.remove_dir(path)
536 }
537
538 fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
539 let from = from.to_owned();
540 let to = to.to_owned();
541 let this = self.clone();
542 Box::pin(async move { this.root.rename(&from, &to).await })
543 }
544
545 fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
546 self.root.metadata(path)
547 }
548
549 fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
550 self.root.symlink_metadata(path)
551 }
552
553 fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
554 self.root.remove_file(path)
555 }
556
557 fn new_open_options(&self) -> OpenOptions<'_> {
558 self.root.new_open_options()
559 }
560}
561
562pub struct WasiFs {
570 pub preopen_fds: RwLock<Vec<u32>>,
572 pub fd_map: RwLock<FdList>,
573 pub current_dir: Mutex<String>,
574 pub root_fs: WasiFsRoot,
575 pub root_inode: InodeGuard,
576 pub has_unioned: Mutex<HashSet<PackageId>>,
577 ephemeral_symlinks: Arc<RwLock<HashMap<PathBuf, EphemeralSymlinkEntry>>>,
578
579 is_wasix: AtomicBool,
584
585 pub(crate) init_preopens: Vec<PreopenedDir>,
587 pub(crate) init_vfs_preopens: Vec<String>,
589}
590
591impl WasiFs {
592 fn writable_package_mount(
593 fs: Arc<dyn FileSystem + Send + Sync>,
594 limiter: Option<&DynFsMemoryLimiter>,
595 ) -> Arc<dyn FileSystem + Send + Sync> {
596 let upper = TmpFileSystem::new();
597 if let Some(limiter) = limiter {
598 upper.set_memory_limiter(limiter.clone());
599 }
600
601 Arc::new(OverlayFileSystem::new(upper, [ArcFileSystem::new(fs)]))
602 }
603
604 pub fn is_wasix(&self) -> bool {
605 self.is_wasix.load(Ordering::Relaxed)
608 }
609
610 pub fn set_is_wasix(&self, is_wasix: bool) {
611 self.is_wasix.store(is_wasix, Ordering::SeqCst);
612 }
613
614 pub(crate) fn register_ephemeral_symlink(
615 &self,
616 full_path: PathBuf,
617 path_to_symlink: PathBuf,
618 relative_path: PathBuf,
619 ) {
620 let mut guard = self.ephemeral_symlinks.write().unwrap();
621 guard.insert(
622 PosixPath::from_path(&full_path)
623 .normalize_virtual_symlink_key()
624 .into_path_buf(),
625 EphemeralSymlinkEntry {
626 path_to_symlink: PosixPath::from_path(&path_to_symlink)
627 .normalize_virtual_symlink_key()
628 .into_path_buf(),
629 relative_path,
630 },
631 );
632 }
633
634 pub(crate) fn ephemeral_symlink_at(&self, full_path: &Path) -> Option<(PathBuf, PathBuf)> {
635 let guard = self.ephemeral_symlinks.read().unwrap();
636 let key = PosixPath::from_path(full_path)
637 .normalize_virtual_symlink_key()
638 .into_path_buf();
639 let entry = guard.get(&key)?;
640 Some((entry.path_to_symlink.clone(), entry.relative_path.clone()))
641 }
642
643 pub(crate) fn unregister_ephemeral_symlink(&self, full_path: &Path) {
644 let mut guard = self.ephemeral_symlinks.write().unwrap();
645 let key = PosixPath::from_path(full_path)
646 .normalize_virtual_symlink_key()
647 .into_path_buf();
648 guard.remove(&key);
649 }
650
651 pub(crate) fn remove_symlink_file(&self, host_path: &Path) -> Errno {
657 match self
658 .root_fs
659 .remove_file(host_path)
660 .map_err(fs_error_into_wasi_err)
661 {
662 Ok(()) => {
663 self.unregister_ephemeral_symlink(host_path);
664 Errno::Success
665 }
666 Err(Errno::Noent) if self.ephemeral_symlink_at(host_path).is_some() => {
667 self.unregister_ephemeral_symlink(host_path);
668 Errno::Success
669 }
670 Err(e) => e,
671 }
672 }
673
674 pub(crate) fn move_ephemeral_symlink(
675 &self,
676 old_full_path: &Path,
677 new_full_path: &Path,
678 path_to_symlink: PathBuf,
679 relative_path: PathBuf,
680 ) {
681 let old_key = PosixPath::from_path(old_full_path)
682 .normalize_virtual_symlink_key()
683 .into_path_buf();
684 let new_key = PosixPath::from_path(new_full_path)
685 .normalize_virtual_symlink_key()
686 .into_path_buf();
687
688 let mut guard = self.ephemeral_symlinks.write().unwrap();
689 guard.remove(&old_key);
690 guard.insert(
691 new_key,
692 EphemeralSymlinkEntry {
693 path_to_symlink: PosixPath::from_path(&path_to_symlink)
694 .normalize_virtual_symlink_key()
695 .into_path_buf(),
696 relative_path,
697 },
698 );
699 }
700
701 pub fn fork(&self) -> Self {
703 Self {
704 preopen_fds: RwLock::new(self.preopen_fds.read().unwrap().clone()),
705 fd_map: RwLock::new(self.fd_map.read().unwrap().clone()),
706 current_dir: Mutex::new(self.current_dir.lock().unwrap().clone()),
707 is_wasix: AtomicBool::new(self.is_wasix.load(Ordering::Acquire)),
708 root_fs: self.root_fs.clone(),
709 root_inode: self.root_inode.clone(),
710 has_unioned: Mutex::new(self.has_unioned.lock().unwrap().clone()),
711 ephemeral_symlinks: self.ephemeral_symlinks.clone(),
712 init_preopens: self.init_preopens.clone(),
713 init_vfs_preopens: self.init_vfs_preopens.clone(),
714 }
715 }
716
717 pub async fn close_cloexec_fds(&self) {
719 let flush_targets = {
720 let mut fd_map = self.fd_map.write().unwrap();
721 let to_close: Vec<WasiFd> = fd_map
722 .iter()
723 .filter_map(|(k, v)| {
724 if v.inner.fd_flags.contains(Fdflagsext::CLOEXEC)
725 && !v.is_stdio
726 && !v.inode.is_preopened
727 {
728 tracing::trace!(fd = %k, "Closing FD due to CLOEXEC flag");
729 Some(k)
730 } else {
731 None
732 }
733 })
734 .collect();
735 let mut flush_targets = Vec::new();
736 for fd in to_close {
737 let outcome = Self::close_fd_locked(&mut fd_map, fd);
738 if let Some(target) = outcome.flush_target {
739 flush_targets.push(target);
740 }
741 }
742 flush_targets
743 };
744
745 for file in flush_targets {
746 Self::flush_file_best_effort(file).await;
747 }
748 }
749
750 pub async fn close_all(&self) {
752 let flush_targets = {
753 let mut fd_map = self.fd_map.write().unwrap();
754 let mut fds: HashSet<WasiFd> = fd_map.keys().collect();
755 fds.insert(__WASI_STDOUT_FILENO);
756 fds.insert(__WASI_STDERR_FILENO);
757
758 let mut flush_targets = Vec::new();
759 for fd in fds {
760 let outcome = Self::close_fd_locked(&mut fd_map, fd);
761 if let Some(target) = outcome.flush_target {
762 flush_targets.push(target);
763 }
764 }
765
766 for (_fd, fd_ref) in fd_map.iter().collect::<Vec<_>>() {
768 if let Some(target) = Self::file_flush_target(&fd_ref.inode) {
769 flush_targets.push(target);
770 }
771 }
772 fd_map.clear();
773 flush_targets
774 };
775
776 for file in flush_targets {
777 Self::flush_file_best_effort(file).await;
778 }
779 }
780
781 pub async fn conditional_union(
784 &self,
785 binary: &BinaryPackage,
786 ) -> Result<(), virtual_fs::FsError> {
787 let Some(package_mounts) = &binary.package_mounts else {
788 return Ok(());
789 };
790
791 let needs_to_be_unioned = self.has_unioned.lock().unwrap().insert(binary.id.clone());
792 if !needs_to_be_unioned {
793 return Ok(());
794 }
795
796 if let Some(root_layer) = &package_mounts.root_layer {
797 self.root_fs
798 .stack_root_filesystem(Self::writable_package_mount(
799 root_layer.clone(),
800 self.root_fs.memory_limiter(),
801 ))?;
802 }
803
804 for mount in &package_mounts.mounts {
805 self.root_fs.root().mount_with_source(
806 &mount.guest_path,
807 &mount.source_path,
808 Self::writable_package_mount(mount.fs.clone(), self.root_fs.memory_limiter()),
809 )?;
810 }
811
812 Ok(())
813 }
814
815 pub(crate) fn new_with_preopen(
817 inodes: &WasiInodes,
818 preopens: &[PreopenedDir],
819 vfs_preopens: &[String],
820 fs_backing: WasiFsRoot,
821 ) -> Result<Self, String> {
822 let mut wasi_fs = Self::new_init(fs_backing, inodes, FS_ROOT_INO)?;
823 wasi_fs.init_preopens = preopens.to_vec();
824 wasi_fs.init_vfs_preopens = vfs_preopens.to_vec();
825 wasi_fs.create_preopens(inodes, false)?;
826 Ok(wasi_fs)
827 }
828
829 pub(crate) fn relative_path_to_absolute(&self, path: String) -> String {
831 if path.starts_with('/') {
832 return path;
833 }
834
835 let current_dir = self.current_dir.lock().unwrap();
836 format!("{}/{}", current_dir.trim_end_matches('/'), path)
837 }
838
839 fn new_init(
842 fs_backing: WasiFsRoot,
843 inodes: &WasiInodes,
844 st_ino: Inode,
845 ) -> Result<Self, String> {
846 debug!("Initializing WASI filesystem");
847
848 let stat = Filestat {
849 st_filetype: Filetype::Directory,
850 st_ino: st_ino.as_u64(),
851 ..Filestat::default()
852 };
853 let root_kind = Kind::Root {
854 entries: HashMap::new(),
855 };
856 let root_inode = inodes.add_inode_val(InodeVal {
857 stat: RwLock::new(stat),
858 is_preopened: true,
859 name: RwLock::new("/".into()),
860 kind: RwLock::new(root_kind),
861 });
862
863 let wasi_fs = Self {
864 preopen_fds: RwLock::new(vec![]),
865 fd_map: RwLock::new(FdList::new()),
866 current_dir: Mutex::new("/".to_string()),
867 is_wasix: AtomicBool::new(false),
868 root_fs: fs_backing,
869 root_inode,
870 has_unioned: Mutex::new(HashSet::new()),
871 ephemeral_symlinks: Arc::new(RwLock::new(HashMap::new())),
872 init_preopens: Default::default(),
873 init_vfs_preopens: Default::default(),
874 };
875 wasi_fs.create_stdin(inodes);
876 wasi_fs.create_stdout(inodes);
877 wasi_fs.create_stderr(inodes);
878 wasi_fs.create_rootfd()?;
879
880 Ok(wasi_fs)
881 }
882
883 #[allow(dead_code)]
893 #[allow(clippy::too_many_arguments)]
894 pub unsafe fn open_dir_all(
895 &mut self,
896 inodes: &WasiInodes,
897 base: WasiFd,
898 name: String,
899 rights: Rights,
900 rights_inheriting: Rights,
901 flags: Fdflags,
902 fd_flags: Fdflagsext,
903 ) -> Result<WasiFd, FsError> {
904 let mut cur_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
907
908 let path: &Path = Path::new(&name);
909 for c in path.components() {
911 let segment_name = c.as_os_str().to_string_lossy().to_string();
912 let guard = cur_inode.read();
913 match guard.deref() {
914 Kind::Dir { entries, .. } | Kind::Root { entries } => {
915 if let Some(_entry) = entries.get(&segment_name) {
916 return Err(FsError::AlreadyExists);
918 }
919
920 let kind = Kind::Dir {
921 parent: cur_inode.downgrade(),
922 path: PathBuf::from(""),
923 entries: HashMap::new(),
924 };
925
926 drop(guard);
927 let inode = self.create_inode_with_default_stat(
928 inodes,
929 kind,
930 false,
931 segment_name.clone().into(),
932 );
933
934 {
936 let mut guard = cur_inode.write();
937 match guard.deref_mut() {
938 Kind::Dir { entries, .. } | Kind::Root { entries } => {
939 entries.insert(segment_name, inode.clone());
940 }
941 _ => unreachable!("Dir or Root became not Dir or Root"),
942 }
943 }
944 cur_inode = inode;
945 }
946 _ => return Err(FsError::BaseNotDirectory),
947 }
948 }
949
950 self.create_fd(
952 rights,
953 rights_inheriting,
954 flags,
955 fd_flags,
956 Fd::READ | Fd::WRITE,
957 cur_inode,
958 )
959 .map_err(fs_error_from_wasi_err)
960 }
961
962 #[allow(dead_code, clippy::too_many_arguments)]
967 pub fn open_file_at(
968 &mut self,
969 inodes: &WasiInodes,
970 base: WasiFd,
971 file: Box<dyn VirtualFile + Send + Sync + 'static>,
972 open_flags: u16,
973 name: String,
974 rights: Rights,
975 rights_inheriting: Rights,
976 flags: Fdflags,
977 fd_flags: Fdflagsext,
978 ) -> Result<WasiFd, FsError> {
979 let base_inode = self.get_fd_inode(base).map_err(fs_error_from_wasi_err)?;
982
983 let guard = base_inode.read();
984 match guard.deref() {
985 Kind::Dir { entries, .. } | Kind::Root { entries } => {
986 if let Some(_entry) = entries.get(&name) {
987 return Err(FsError::AlreadyExists);
989 }
990
991 let kind = Kind::File {
992 handle: Some(Arc::new(RwLock::new(file))),
993 path: PathBuf::from(""),
994 fd: None,
995 };
996
997 drop(guard);
998 let inode = self
999 .create_inode(inodes, kind, false, name.clone())
1000 .map_err(|_| FsError::IOError)?;
1001
1002 {
1003 let mut guard = base_inode.write();
1004 match guard.deref_mut() {
1005 Kind::Dir { entries, .. } | Kind::Root { entries } => {
1006 entries.insert(name, inode.clone());
1007 }
1008 _ => unreachable!("Dir or Root became not Dir or Root"),
1009 }
1010 }
1011
1012 let real_fd = self
1014 .create_fd(
1015 rights,
1016 rights_inheriting,
1017 flags,
1018 fd_flags,
1019 open_flags,
1020 inode.clone(),
1021 )
1022 .map_err(fs_error_from_wasi_err)?;
1023
1024 {
1025 let mut guard = inode.kind.write().unwrap();
1026 match guard.deref_mut() {
1027 Kind::File { fd, .. } => {
1028 *fd = Some(real_fd);
1029 }
1030 _ => unreachable!("We just created a Kind::File"),
1031 }
1032 }
1033
1034 Ok(real_fd)
1035 }
1036 _ => Err(FsError::BaseNotDirectory),
1037 }
1038 }
1039
1040 #[allow(dead_code)]
1044 pub fn swap_file(
1045 &self,
1046 fd: WasiFd,
1047 mut file: Box<dyn VirtualFile + Send + Sync + 'static>,
1048 ) -> Result<Option<Box<dyn VirtualFile + Send + Sync + 'static>>, FsError> {
1049 match fd {
1050 __WASI_STDIN_FILENO => {
1051 let mut target = WasiInodes::stdin_mut(&self.fd_map)?;
1052 Ok(Some(target.swap(file)))
1053 }
1054 __WASI_STDOUT_FILENO => {
1055 let mut target = WasiInodes::stdout_mut(&self.fd_map)?;
1056 Ok(Some(target.swap(file)))
1057 }
1058 __WASI_STDERR_FILENO => {
1059 let mut target = WasiInodes::stderr_mut(&self.fd_map)?;
1060 Ok(Some(target.swap(file)))
1061 }
1062 _ => {
1063 let base_inode = self.get_fd_inode(fd).map_err(fs_error_from_wasi_err)?;
1064 {
1065 let guard = base_inode.read();
1067 match guard.deref() {
1068 Kind::File { handle, .. } => {
1069 if let Some(handle) = handle {
1070 let mut handle = handle.write().unwrap();
1071 std::mem::swap(handle.deref_mut(), &mut file);
1072 return Ok(Some(file));
1073 }
1074 }
1075 _ => return Err(FsError::NotAFile),
1076 }
1077 }
1078 let mut guard = base_inode.write();
1080 match guard.deref_mut() {
1081 Kind::File { handle, .. } => {
1082 if let Some(handle) = handle {
1083 let mut handle = handle.write().unwrap();
1084 std::mem::swap(handle.deref_mut(), &mut file);
1085 Ok(Some(file))
1086 } else {
1087 handle.replace(Arc::new(RwLock::new(file)));
1088 Ok(None)
1089 }
1090 }
1091 _ => Err(FsError::NotAFile),
1092 }
1093 }
1094 }
1095 }
1096
1097 pub fn filestat_resync_size(&self, fd: WasiFd) -> Result<Filesize, Errno> {
1099 let inode = self.get_fd_inode(fd)?;
1100 let mut guard = inode.write();
1101 match guard.deref_mut() {
1102 Kind::File { handle, .. } => {
1103 if let Some(h) = handle {
1104 let h = h.read().unwrap();
1105 let new_size = h.size();
1106 drop(h);
1107 drop(guard);
1108
1109 inode.stat.write().unwrap().st_size = new_size;
1110 Ok(new_size as Filesize)
1111 } else {
1112 Err(Errno::Badf)
1113 }
1114 }
1115 Kind::Dir { .. } | Kind::Root { .. } => Err(Errno::Isdir),
1116 _ => Err(Errno::Inval),
1117 }
1118 }
1119
1120 pub fn set_current_dir(&self, path: &str) {
1122 let mut guard = self.current_dir.lock().unwrap();
1123 *guard = path.to_string();
1124 }
1125
1126 pub fn get_current_dir(
1128 &self,
1129 inodes: &WasiInodes,
1130 base: WasiFd,
1131 ) -> Result<(InodeGuard, String), Errno> {
1132 self.get_current_dir_inner(inodes, base, 0)
1133 }
1134
1135 pub(crate) fn get_current_dir_inner(
1136 &self,
1137 inodes: &WasiInodes,
1138 base: WasiFd,
1139 symlink_count: u32,
1140 ) -> Result<(InodeGuard, String), Errno> {
1141 let mut symlink_count = symlink_count;
1142 let current_dir = {
1143 let guard = self.current_dir.lock().unwrap();
1144 guard.clone()
1145 };
1146 let cur_inode = self.get_fd_inode(base)?;
1147 let inode = self.get_inode_at_path_inner(
1148 inodes,
1149 cur_inode,
1150 current_dir.as_str(),
1151 &mut symlink_count,
1152 true,
1153 )?;
1154 Ok((inode, current_dir))
1155 }
1156
1157 fn get_inode_at_path_inner(
1242 &self,
1243 inodes: &WasiInodes,
1244 mut cur_inode: InodeGuard,
1245 path_str: &str,
1246 symlink_count: &mut u32,
1247 follow_symlinks: bool,
1248 ) -> Result<InodeGuard, Errno> {
1249 if *symlink_count > MAX_SYMLINKS {
1250 return Err(Errno::Loop);
1251 }
1252
1253 if path_str.is_empty() {
1254 return Err(Errno::Noent);
1255 }
1256
1257 if path_str.starts_with('/') {
1258 cur_inode = self.get_fd_inode(VIRTUAL_ROOT_FD)?;
1259 }
1260
1261 let is_all_slashes = path_str.bytes().all(|b| b == b'/');
1262
1263 if is_all_slashes {
1266 if let Kind::Root { entries } = cur_inode.read().deref()
1267 && let Some(root_entry) = entries.get("/")
1268 {
1269 return Ok(root_entry.clone());
1270 }
1271 return Ok(cur_inode);
1272 }
1273
1274 let path = PosixPath::new(path_str);
1278 let components = path.components(true, true);
1279
1280 let n_components = components.len();
1281
1282 'path_iter: for (i, component) in components.into_iter().enumerate() {
1286 if matches!(component, PosixPathComponent::RootDir) {
1290 continue 'path_iter;
1291 }
1292
1293 let last_component = i == n_components - 1;
1301
1302 let component_str = match component {
1303 PosixPathComponent::CurDir => {
1304 let is_dir = {
1305 let guard = cur_inode.read();
1306 matches!(guard.deref(), Kind::Dir { .. } | Kind::Root { .. })
1307 };
1308 if is_dir {
1309 continue 'path_iter;
1310 }
1311 return Err(Errno::Notdir);
1312 }
1313 PosixPathComponent::ParentDir => {
1314 let parent_inode = {
1315 let guard = cur_inode.read();
1316 match guard.deref() {
1317 Kind::Root { .. } => None,
1318 Kind::Dir { parent, .. } => {
1319 Some(parent.upgrade().ok_or(Errno::Access)?)
1320 }
1321 _ => return Err(Errno::Notdir),
1322 }
1323 };
1324 if let Some(parent_inode) = parent_inode {
1325 cur_inode = parent_inode;
1326 }
1327 continue 'path_iter;
1328 }
1329 PosixPathComponent::Normal(component) => component,
1330 PosixPathComponent::RootDir => unreachable!("RootDir is handled above"),
1331 };
1332
1333 'component_lookup: loop {
1334 let component_resolution = {
1344 match cur_inode.clone().read().deref() {
1345 Kind::Buffer { .. } => {
1346 unimplemented!("state::get_inode_at_path for buffers")
1347 }
1348 Kind::File { .. }
1349 | Kind::Socket { .. }
1350 | Kind::PipeRx { .. }
1351 | Kind::PipeTx { .. }
1352 | Kind::DuplexPipe { .. }
1353 | Kind::EventNotifications { .. }
1354 | Kind::Epoll { .. } => {
1355 return Err(Errno::Notdir);
1356 }
1357 Kind::Symlink { .. } => break 'component_lookup,
1358 Kind::Root { entries } => {
1359 if let Some(entry) = entries.get(component_str) {
1360 cur_inode = entry.clone();
1361 break 'component_lookup;
1362 } else if let Some(root) = entries.get("/") {
1363 cur_inode = root.clone();
1371 continue 'component_lookup;
1372 } else {
1373 return Err(Errno::Notcapable);
1376 }
1377 }
1378 Kind::Dir {
1379 entries,
1380 path: cur_dir,
1381 ..
1382 } => {
1383 if let Some(entry) = entries.get(component_str) {
1390 cur_inode = entry.clone();
1394 break 'component_lookup;
1395 }
1396
1397 let entry_path_buf = PosixPath::from_path(cur_dir)
1400 .join(&PosixPath::new(component_str))
1401 .into_path_buf();
1402
1403 let entry_name = component_str.to_string();
1406
1407 let entry_path = entry_path_buf.to_string_lossy().to_string();
1413
1414 if let Some((path_to_symlink, relative_path)) =
1415 self.ephemeral_symlink_at(&entry_path_buf)
1416 {
1417 ComponentResolution::Create {
1424 kind: Kind::Symlink {
1425 symlink_kind: SymlinkKind::Virtual,
1426 path_to_symlink,
1427 relative_path,
1428 },
1429 name: entry_path,
1430 entry_name,
1431 is_ephemeral: true,
1432 }
1433 } else {
1434 let metadata = self
1440 .root_fs
1441 .symlink_metadata(&entry_path_buf)
1442 .ok()
1443 .ok_or(Errno::Noent)?;
1444 let file_type = metadata.file_type();
1445 if file_type.is_dir() {
1446 ComponentResolution::Create {
1448 kind: Kind::Dir {
1449 parent: cur_inode.downgrade(),
1450 path: entry_path_buf,
1451 entries: Default::default(),
1452 },
1453 name: entry_path,
1454 entry_name,
1455 is_ephemeral: false,
1456 }
1457 } else if file_type.is_file() {
1458 ComponentResolution::Create {
1460 kind: Kind::File {
1461 handle: None,
1462 path: entry_path_buf,
1463 fd: None,
1464 },
1465 name: entry_path,
1466 entry_name,
1467 is_ephemeral: false,
1468 }
1469 } else if file_type.is_symlink() {
1470 let link_value = self
1477 .root_fs
1478 .readlink(&entry_path_buf)
1479 .ok()
1480 .ok_or(Errno::Noent)?;
1481 debug!("attempting to decompose path {:?}", link_value);
1482 ComponentResolution::BackingSymlink {
1483 file: entry_path_buf,
1484 link_value,
1485 entry_name,
1486 }
1487 } else {
1488 #[cfg(unix)]
1489 {
1490 let file_type: Filetype = if file_type.is_char_device() {
1492 Filetype::CharacterDevice
1493 } else if file_type.is_block_device() {
1494 Filetype::BlockDevice
1495 } else if file_type.is_fifo() {
1496 Filetype::Unknown
1498 } else if file_type.is_socket() {
1499 Filetype::SocketStream
1502 } else {
1503 unimplemented!(
1504 "state::get_inode_at_path unknown file type: not file, directory, symlink, char device, block device, fifo, or socket"
1505 );
1506 };
1507
1508 ComponentResolution::Special {
1509 kind: Kind::File {
1510 handle: None,
1511 path: entry_path_buf,
1512 fd: None,
1513 },
1514 name: entry_path.into(),
1515 entry_name,
1516 stat: Filestat {
1517 st_filetype: file_type,
1518 st_ino: Inode::from_path(path_str).as_u64(),
1519 st_size: metadata.len(),
1520 st_ctim: metadata.created(),
1521 st_mtim: metadata.modified(),
1522 st_atim: metadata.accessed(),
1523 ..Filestat::default()
1524 },
1525 }
1526 }
1527 #[cfg(not(unix))]
1528 unimplemented!(
1529 "state::get_inode_at_path unknown file type: not file, directory, or symlink"
1530 );
1531 }
1532 } } } }; let (entry_name, new_inode, should_insert, should_return) =
1543 match component_resolution {
1544 ComponentResolution::Create {
1545 kind,
1546 name,
1547 entry_name,
1548 is_ephemeral,
1549 } => {
1550 let new_inode = self.create_inode(inodes, kind, false, name)?;
1551 (entry_name, new_inode, !is_ephemeral, false)
1552 }
1553 ComponentResolution::BackingSymlink {
1554 file,
1555 link_value,
1556 entry_name,
1557 } => {
1558 let new_inode = self.create_inode(
1559 inodes,
1560 Kind::Symlink {
1561 symlink_kind: SymlinkKind::Backing,
1562 path_to_symlink: PosixPath::from_path(&file)
1563 .strip_root_prefix()
1564 .into_path_buf(),
1565 relative_path: link_value,
1566 },
1567 false,
1568 file.to_string_lossy().to_string(),
1569 )?;
1570 (entry_name, new_inode, false, false)
1571 }
1572 #[cfg(unix)]
1573 ComponentResolution::Special {
1574 kind,
1575 name,
1576 entry_name,
1577 stat,
1578 } => {
1579 let new_inode =
1580 self.create_inode_with_stat(inodes, kind, false, name, stat);
1581 (entry_name, new_inode, true, true)
1582 }
1583 };
1584
1585 {
1586 let mut guard = cur_inode.write();
1587 let Kind::Dir { entries, .. } = guard.deref_mut() else {
1588 unreachable!("Attempted to insert special device into non-directory");
1589 };
1590
1591 if should_insert {
1592 entries.insert(entry_name, new_inode.clone());
1593 }
1594
1595 if should_return {
1596 if last_component {
1598 return Ok(new_inode);
1599 }
1600 return Err(Errno::Notdir);
1601 }
1602 }
1603
1604 cur_inode = new_inode;
1609 break 'component_lookup;
1610 } if last_component && !follow_symlinks {
1616 continue 'path_iter;
1620 }
1621
1622 let (symlink_kind, path_to_symlink, relative_path) = {
1624 let guard = cur_inode.read();
1625 let Kind::Symlink {
1626 symlink_kind,
1627 path_to_symlink,
1628 relative_path,
1629 } = guard.deref()
1630 else {
1631 continue 'path_iter;
1633 };
1634 (
1635 *symlink_kind,
1636 path_to_symlink.clone(),
1637 relative_path.clone(),
1638 )
1639 };
1640
1641 let (new_base_fd, new_path) =
1642 self.resolve_symlink_target_path(symlink_kind, &path_to_symlink, &relative_path)?;
1643 let new_base_inode = self.get_fd_inode(new_base_fd)?;
1644 let new_path = PosixPath::from_path(&new_path).as_str().to_owned();
1645
1646 let follow_symlinks_inner = !last_component || follow_symlinks;
1650
1651 debug!("Following symlink recursively");
1652 *symlink_count += 1;
1653 if *symlink_count > MAX_SYMLINKS {
1654 return Err(Errno::Loop);
1655 }
1656 let symlink_inode = self.get_inode_at_path_inner(
1657 inodes,
1658 new_base_inode,
1659 &new_path,
1660 symlink_count,
1661 follow_symlinks_inner,
1662 )?;
1663
1664 cur_inode = symlink_inode;
1667 }
1668
1669 Ok(cur_inode)
1670 }
1671
1672 pub(crate) fn resolve_symlink_target_path(
1673 &self,
1674 symlink_kind: SymlinkKind,
1675 path_to_symlink: &Path,
1676 relative_path: &Path,
1677 ) -> Result<(WasiFd, PathBuf), Errno> {
1678 let relative_posix = PosixPath::from_path(relative_path);
1679 if matches!(symlink_kind, SymlinkKind::Virtual) && relative_posix.is_absolute() {
1680 return Ok((VIRTUAL_ROOT_FD, relative_path.to_owned()));
1681 }
1682
1683 let symlink_parent = match symlink_kind {
1684 SymlinkKind::Virtual => PosixPath::from_path(path_to_symlink)
1685 .parent()
1686 .into_path_buf(),
1687 SymlinkKind::Backing => {
1688 let symlink_path_buf =
1689 PosixPath::new("/").join(&PosixPath::from_path(path_to_symlink));
1690 let symlink_path = symlink_path_buf.as_posix_path();
1691 let mount_entry = self
1692 .root_fs
1693 .root()
1694 .mount_entries()
1695 .into_iter()
1696 .filter(|entry| {
1697 symlink_path
1698 .strip_prefix(&PosixPath::from_path(&entry.path))
1699 .is_some()
1700 })
1701 .max_by_key(|entry| PosixPath::from_path(&entry.path).as_str().len())
1702 .ok_or(Errno::Perm)?;
1703 let mount_path = mount_entry.path;
1704
1705 let symlink_relative = symlink_path
1706 .strip_prefix(&PosixPath::from_path(&mount_path))
1707 .ok_or(Errno::Perm)?;
1708 let symlink_parent = symlink_relative.parent().into_path_buf();
1709 let contained_target = if relative_posix.is_absolute() {
1710 let stripped = relative_posix
1711 .strip_prefix(&PosixPath::from_path(&mount_entry.source_path))
1712 .ok_or(Errno::Perm)?;
1713 PosixPathBuf::from(stripped.as_str().to_owned())
1714 } else {
1715 PosixPathBuf::resolve_relative(
1716 &PosixPath::from_path(&symlink_parent),
1717 &relative_posix,
1718 false,
1719 )?
1720 };
1721
1722 return Ok((
1723 VIRTUAL_ROOT_FD,
1724 PosixPath::from_path(&mount_path)
1725 .join(&contained_target.as_posix_path())
1726 .into_path_buf(),
1727 ));
1728 }
1729 };
1730
1731 Ok((
1732 VIRTUAL_ROOT_FD,
1733 PosixPathBuf::resolve_relative(
1734 &PosixPath::from_path(&symlink_parent),
1735 &relative_posix,
1736 true,
1737 )?
1738 .into_path_buf(),
1739 ))
1740 }
1741
1742 pub(crate) fn rebase_symlink_location(&self, new_symlink_path: &Path) -> PathBuf {
1743 PosixPath::from_path(new_symlink_path)
1744 .strip_root_prefix()
1745 .into_path_buf()
1746 }
1747
1748 pub(crate) fn get_inode_at_path(
1755 &self,
1756 inodes: &WasiInodes,
1757 base: WasiFd,
1758 path: &str,
1759 follow_symlinks: bool,
1760 ) -> Result<InodeGuard, Errno> {
1761 let base_inode = self.get_fd_inode(base)?;
1762 let mut symlink_count = 0;
1763 self.get_inode_at_path_inner(
1764 inodes,
1765 base_inode,
1766 path,
1767 &mut symlink_count,
1768 follow_symlinks,
1769 )
1770 }
1771
1772 pub(crate) fn get_inode_at_path_from_inode(
1773 &self,
1774 inodes: &WasiInodes,
1775 base_inode: InodeGuard,
1776 path: &str,
1777 follow_symlinks: bool,
1778 ) -> Result<InodeGuard, Errno> {
1779 let mut symlink_count = 0;
1780 self.get_inode_at_path_inner(
1781 inodes,
1782 base_inode,
1783 path,
1784 &mut symlink_count,
1785 follow_symlinks,
1786 )
1787 }
1788
1789 pub(crate) fn get_parent_inode_at_path(
1792 &self,
1793 inodes: &WasiInodes,
1794 base: WasiFd,
1795 path: &Path,
1796 follow_symlinks: bool,
1797 ) -> Result<(InodeGuard, String), Errno> {
1798 let (parent_dir, new_entity_name) = PosixPath::from_path(path).parent_path_and_name()?;
1799 if parent_dir.as_str().is_empty() {
1800 return self.get_fd_inode(base).map(|v| (v, new_entity_name));
1801 }
1802 self.get_inode_at_path(inodes, base, parent_dir.as_str(), follow_symlinks)
1803 .map(|v| (v, new_entity_name))
1804 }
1805
1806 pub fn get_fd(&self, fd: WasiFd) -> Result<Fd, Errno> {
1807 let ret = self
1808 .fd_map
1809 .read()
1810 .unwrap()
1811 .get(fd)
1812 .ok_or(Errno::Badf)
1813 .cloned();
1814
1815 if ret.is_err() && fd == VIRTUAL_ROOT_FD {
1816 Ok(Self::virtual_root_fd(self.root_inode.clone()))
1817 } else {
1818 ret
1819 }
1820 }
1821
1822 pub fn get_fd_inode(&self, fd: WasiFd) -> Result<InodeGuard, Errno> {
1823 if fd == VIRTUAL_ROOT_FD {
1825 return Ok(self.root_inode.clone());
1826 }
1827 self.fd_map
1828 .read()
1829 .unwrap()
1830 .get(fd)
1831 .ok_or(Errno::Badf)
1832 .map(|a| a.inode.clone())
1833 }
1834
1835 pub fn filestat_fd(&self, fd: WasiFd) -> Result<Filestat, Errno> {
1836 let inode = self.get_fd_inode(fd)?;
1837 let guard = inode.stat.read().unwrap();
1838 Ok(*guard.deref())
1839 }
1840
1841 pub fn fdstat(&self, fd: WasiFd) -> Result<Fdstat, Errno> {
1842 match fd {
1843 __WASI_STDIN_FILENO => {
1844 return Ok(Fdstat {
1845 fs_filetype: Filetype::CharacterDevice,
1846 fs_flags: Fdflags::empty(),
1847 fs_rights_base: STDIN_DEFAULT_RIGHTS,
1848 fs_rights_inheriting: Rights::empty(),
1849 });
1850 }
1851 __WASI_STDOUT_FILENO => {
1852 return Ok(Fdstat {
1853 fs_filetype: Filetype::CharacterDevice,
1854 fs_flags: Fdflags::APPEND,
1855 fs_rights_base: STDOUT_DEFAULT_RIGHTS,
1856 fs_rights_inheriting: Rights::empty(),
1857 });
1858 }
1859 __WASI_STDERR_FILENO => {
1860 return Ok(Fdstat {
1861 fs_filetype: Filetype::CharacterDevice,
1862 fs_flags: Fdflags::APPEND,
1863 fs_rights_base: STDERR_DEFAULT_RIGHTS,
1864 fs_rights_inheriting: Rights::empty(),
1865 });
1866 }
1867 VIRTUAL_ROOT_FD => {
1868 return Ok(Fdstat {
1869 fs_filetype: Filetype::Directory,
1870 fs_flags: Fdflags::empty(),
1871 fs_rights_base: ALL_RIGHTS,
1873 fs_rights_inheriting: ALL_RIGHTS,
1874 });
1875 }
1876 _ => (),
1877 }
1878 let fd = self.get_fd(fd)?;
1879
1880 let guard = fd.inode.read();
1881 let deref = guard.deref();
1882 Ok(Fdstat {
1883 fs_filetype: match deref {
1884 Kind::File { .. } => Filetype::RegularFile,
1885 Kind::Dir { .. } => Filetype::Directory,
1886 Kind::Symlink { .. } => Filetype::SymbolicLink,
1887 Kind::Socket { socket } => match &socket.inner.protected.read().unwrap().kind {
1888 InodeSocketKind::TcpStream { .. } => Filetype::SocketStream,
1889 InodeSocketKind::Raw { .. } => Filetype::SocketRaw,
1890 InodeSocketKind::PreSocket { props, .. } => match props.ty {
1891 Socktype::Stream => Filetype::SocketStream,
1892 Socktype::Dgram => Filetype::SocketDgram,
1893 Socktype::Raw => Filetype::SocketRaw,
1894 Socktype::Seqpacket => Filetype::SocketSeqpacket,
1895 _ => Filetype::Unknown,
1896 },
1897 _ => Filetype::Unknown,
1898 },
1899 _ => Filetype::Unknown,
1900 },
1901 fs_flags: fd.inner.flags,
1902 fs_rights_base: fd.inner.rights,
1903 fs_rights_inheriting: fd.inner.rights_inheriting, })
1905 }
1906
1907 pub fn prestat_fd(&self, fd: WasiFd) -> Result<Prestat, Errno> {
1908 let inode = self.get_fd_inode(fd)?;
1909 if inode.is_preopened {
1912 Ok(self.prestat_fd_inner(inode.deref()))
1913 } else {
1914 Err(Errno::Badf)
1915 }
1916 }
1917
1918 pub(crate) fn prestat_fd_inner(&self, inode_val: &InodeVal) -> Prestat {
1919 Prestat {
1920 pr_type: Preopentype::Dir,
1921 u: PrestatEnum::Dir {
1922 pr_name_len: inode_val.name.read().unwrap().len() as u32,
1924 }
1925 .untagged(),
1926 }
1927 }
1928
1929 pub(crate) fn create_inode(
1931 &self,
1932 inodes: &WasiInodes,
1933 kind: Kind,
1934 is_preopened: bool,
1935 name: String,
1936 ) -> Result<InodeGuard, Errno> {
1937 let stat = self.get_stat_for_kind(&kind)?;
1938 Ok(self.create_inode_with_stat(inodes, kind, is_preopened, name.into(), stat))
1939 }
1940
1941 pub(crate) fn create_inode_with_default_stat(
1943 &self,
1944 inodes: &WasiInodes,
1945 kind: Kind,
1946 is_preopened: bool,
1947 name: Cow<'static, str>,
1948 ) -> InodeGuard {
1949 let stat = Filestat::default();
1950 self.create_inode_with_stat(inodes, kind, is_preopened, name, stat)
1951 }
1952
1953 pub(crate) fn create_inode_with_stat(
1955 &self,
1956 inodes: &WasiInodes,
1957 kind: Kind,
1958 is_preopened: bool,
1959 name: Cow<'static, str>,
1960 mut stat: Filestat,
1961 ) -> InodeGuard {
1962 match &kind {
1963 Kind::File {
1964 handle: Some(handle),
1965 ..
1966 } => {
1967 let guard = handle.read().unwrap();
1968 stat.st_size = guard.size();
1969 }
1970 Kind::Buffer { buffer } => {
1971 stat.st_size = buffer.len() as u64;
1972 }
1973 _ => {}
1974 }
1975
1976 let inode_key: Cow<'_, str> = match &kind {
1977 Kind::File { path, .. } | Kind::Dir { path, .. } => {
1978 let path_str = path.to_string_lossy();
1979 if path_str.is_empty() {
1980 Cow::Borrowed(name.as_ref())
1981 } else {
1982 path_str
1983 }
1984 }
1985 Kind::Symlink {
1986 path_to_symlink, ..
1987 } => {
1988 let path_str = path_to_symlink.to_string_lossy();
1989 if path_str.is_empty() {
1990 Cow::Borrowed(name.as_ref())
1991 } else {
1992 path_str
1993 }
1994 }
1995 _ => Cow::Borrowed(name.as_ref()),
1996 };
1997
1998 let st_ino = Inode::from_path(&inode_key);
1999 stat.st_ino = st_ino.as_u64();
2000
2001 inodes.add_inode_val(InodeVal {
2002 stat: RwLock::new(stat),
2003 is_preopened,
2004 name: RwLock::new(name),
2005 kind: RwLock::new(kind),
2006 })
2007 }
2008
2009 fn make_fd(
2010 rights: Rights,
2011 rights_inheriting: Rights,
2012 fs_flags: Fdflags,
2013 fd_flags: Fdflagsext,
2014 open_flags: u16,
2015 inode: InodeGuard,
2016 idx: Option<WasiFd>,
2017 ) -> Fd {
2018 let is_stdio = matches!(
2019 idx,
2020 Some(__WASI_STDIN_FILENO) | Some(__WASI_STDOUT_FILENO) | Some(__WASI_STDERR_FILENO)
2021 );
2022 Fd {
2023 inner: FdInner {
2024 rights,
2025 rights_inheriting,
2026 flags: fs_flags,
2027 offset: Arc::new(AtomicU64::new(0)),
2028 fd_flags,
2029 },
2030 open_flags,
2031 inode,
2032 is_stdio,
2033 }
2034 }
2035
2036 #[allow(clippy::too_many_arguments)]
2041 pub(crate) fn insert_fd_locked(
2042 fd_map: &mut FdList,
2043 rights: Rights,
2044 rights_inheriting: Rights,
2045 fs_flags: Fdflags,
2046 fd_flags: Fdflagsext,
2047 open_flags: u16,
2048 inode: InodeGuard,
2049 idx: Option<WasiFd>,
2050 exclusive: bool,
2051 ) -> Result<WasiFd, Errno> {
2052 let fd = Self::make_fd(
2053 rights,
2054 rights_inheriting,
2055 fs_flags,
2056 fd_flags,
2057 open_flags,
2058 inode,
2059 idx,
2060 );
2061
2062 match idx {
2063 Some(idx) => {
2064 if idx > MAX_FD {
2065 return Err(Errno::Badf);
2066 }
2067 if fd_map.insert(exclusive, idx, fd) {
2068 Ok(idx)
2069 } else {
2070 Err(Errno::Exist)
2071 }
2072 }
2073 None => Ok(fd_map.insert_first_free(fd)),
2074 }
2075 }
2076
2077 pub(crate) fn clone_fd_locked(
2079 fs: &WasiFs,
2080 fd_map: &mut FdList,
2081 fd: WasiFd,
2082 min_result_fd: WasiFd,
2083 cloexec: Option<bool>,
2084 ) -> Result<WasiFd, Errno> {
2085 let fd = Self::get_fd_from_locked_map(fs, fd_map, fd)?;
2086 Self::ensure_file_handle_present(&fd)?;
2087 if min_result_fd > MAX_FD {
2088 return Err(Errno::Inval);
2089 }
2090 Ok(fd_map.insert_first_free_after(
2091 Fd {
2092 inner: FdInner {
2093 rights: fd.inner.rights,
2094 rights_inheriting: fd.inner.rights_inheriting,
2095 flags: fd.inner.flags,
2096 offset: fd.inner.offset.clone(),
2097 fd_flags: match cloexec {
2098 None => fd.inner.fd_flags,
2099 Some(cloexec) => {
2100 let mut f = fd.inner.fd_flags;
2101 f.set(Fdflagsext::CLOEXEC, cloexec);
2102 f
2103 }
2104 },
2105 },
2106 open_flags: fd.open_flags,
2107 inode: fd.inode,
2108 is_stdio: fd.is_stdio,
2109 },
2110 min_result_fd,
2111 ))
2112 }
2113
2114 pub(crate) fn get_fd_from_locked_map(
2116 fs: &WasiFs,
2117 fd_map: &FdList,
2118 fd: WasiFd,
2119 ) -> Result<Fd, Errno> {
2120 match fd_map.get(fd) {
2121 Some(fd) => Ok(fd.clone()),
2122 None if fd == VIRTUAL_ROOT_FD => Ok(Self::virtual_root_fd(fs.root_inode.clone())),
2123 None => Err(Errno::Badf),
2124 }
2125 }
2126
2127 fn virtual_root_fd(root_inode: InodeGuard) -> Fd {
2128 Fd {
2129 inner: FdInner {
2130 rights: ALL_RIGHTS,
2131 rights_inheriting: ALL_RIGHTS,
2132 flags: Fdflags::empty(),
2133 offset: Arc::new(AtomicU64::new(0)),
2134 fd_flags: Fdflagsext::empty(),
2135 },
2136 open_flags: 0,
2137 inode: root_inode,
2138 is_stdio: false,
2139 }
2140 }
2141
2142 fn ensure_file_handle_present(fd: &Fd) -> Result<(), Errno> {
2143 let guard = fd.inode.read();
2144 match guard.deref() {
2145 Kind::File { handle: None, .. } => Err(Errno::Badf),
2146 _ => Ok(()),
2147 }
2148 }
2149
2150 pub(crate) fn dup2_at(
2156 &self,
2157 src: WasiFd,
2158 dst: WasiFd,
2159 ) -> Result<Option<VirtualFileLock>, Errno> {
2160 if dst > MAX_FD {
2161 return Err(Errno::Badf);
2162 }
2163
2164 let flush_target = {
2165 let mut fd_map = self.fd_map.write().unwrap();
2166
2167 let fd_entry = fd_map.get(src).ok_or(Errno::Badf)?;
2168 Self::ensure_file_handle_present(fd_entry)?;
2169
2170 if src == dst {
2171 return Ok(None);
2172 }
2173
2174 if let Some(target_fd) = fd_map.get(dst)
2175 && !target_fd.is_stdio
2176 && target_fd.inode.is_preopened
2177 {
2178 warn!("Refusing dup2({src}, {dst}) because FD {dst} is pre-opened");
2179 return Err(Errno::Notsup);
2180 }
2181
2182 let new_fd_entry = Fd {
2183 inner: FdInner {
2184 offset: fd_entry.inner.offset.clone(),
2185 rights: fd_entry.inner.rights_inheriting,
2186 fd_flags: {
2187 let mut f = fd_entry.inner.fd_flags;
2188 f.set(Fdflagsext::CLOEXEC, false);
2189 f
2190 },
2191 ..fd_entry.inner
2192 },
2193 inode: fd_entry.inode.clone(),
2194 ..*fd_entry
2195 };
2196
2197 let flush_target = fd_map
2198 .get(dst)
2199 .and_then(|fd| Self::file_flush_target(&fd.inode));
2200
2201 fd_map.remove(dst);
2202
2203 if !fd_map.insert(true, dst, new_fd_entry) {
2204 panic!("Internal error: expected FD {dst} to be free after remove in dup2_at");
2205 }
2206
2207 flush_target
2208 };
2209
2210 Ok(flush_target)
2211 }
2212
2213 pub fn create_fd(
2214 &self,
2215 rights: Rights,
2216 rights_inheriting: Rights,
2217 fs_flags: Fdflags,
2218 fd_flags: Fdflagsext,
2219 open_flags: u16,
2220 inode: InodeGuard,
2221 ) -> Result<WasiFd, Errno> {
2222 self.create_fd_ext(
2223 rights,
2224 rights_inheriting,
2225 fs_flags,
2226 fd_flags,
2227 open_flags,
2228 inode,
2229 None,
2230 false,
2231 )
2232 }
2233
2234 #[allow(clippy::too_many_arguments)]
2235 pub fn with_fd(
2236 &self,
2237 rights: Rights,
2238 rights_inheriting: Rights,
2239 fs_flags: Fdflags,
2240 fd_flags: Fdflagsext,
2241 open_flags: u16,
2242 inode: InodeGuard,
2243 idx: WasiFd,
2244 ) -> Result<(), Errno> {
2245 self.create_fd_ext(
2246 rights,
2247 rights_inheriting,
2248 fs_flags,
2249 fd_flags,
2250 open_flags,
2251 inode,
2252 Some(idx),
2253 true,
2254 )?;
2255 Ok(())
2256 }
2257
2258 #[allow(clippy::too_many_arguments)]
2259 pub fn create_fd_ext(
2260 &self,
2261 rights: Rights,
2262 rights_inheriting: Rights,
2263 fs_flags: Fdflags,
2264 fd_flags: Fdflagsext,
2265 open_flags: u16,
2266 inode: InodeGuard,
2267 idx: Option<WasiFd>,
2268 exclusive: bool,
2269 ) -> Result<WasiFd, Errno> {
2270 let mut fd_map = self.fd_map.write().unwrap();
2271 Self::insert_fd_locked(
2272 &mut fd_map,
2273 rights,
2274 rights_inheriting,
2275 fs_flags,
2276 fd_flags,
2277 open_flags,
2278 inode,
2279 idx,
2280 exclusive,
2281 )
2282 }
2283
2284 pub fn clone_fd(&self, fd: WasiFd) -> Result<WasiFd, Errno> {
2285 self.clone_fd_ext(fd, 0, None)
2286 }
2287
2288 pub fn clone_fd_ext(
2289 &self,
2290 fd: WasiFd,
2291 min_result_fd: WasiFd,
2292 cloexec: Option<bool>,
2293 ) -> Result<WasiFd, Errno> {
2294 let mut fd_map = self.fd_map.write().unwrap();
2295 Self::clone_fd_locked(self, &mut fd_map, fd, min_result_fd, cloexec)
2296 }
2297
2298 pub unsafe fn remove_inode(&self, inodes: &WasiInodes, ino: Inode) -> Option<Arc<InodeVal>> {
2307 let mut guard = inodes.protected.write().unwrap();
2308 guard.lookup.remove(&ino).and_then(|a| Weak::upgrade(&a))
2309 }
2310
2311 pub(crate) fn create_stdout(&self, inodes: &WasiInodes) {
2312 self.create_std_dev_inner(
2313 inodes,
2314 Box::<Stdout>::default(),
2315 "stdout",
2316 __WASI_STDOUT_FILENO,
2317 STDOUT_DEFAULT_RIGHTS,
2318 Fdflags::APPEND,
2319 FS_STDOUT_INO,
2320 );
2321 }
2322
2323 pub(crate) fn create_stdin(&self, inodes: &WasiInodes) {
2324 self.create_std_dev_inner(
2325 inodes,
2326 Box::<Stdin>::default(),
2327 "stdin",
2328 __WASI_STDIN_FILENO,
2329 STDIN_DEFAULT_RIGHTS,
2330 Fdflags::empty(),
2331 FS_STDIN_INO,
2332 );
2333 }
2334
2335 pub(crate) fn create_stderr(&self, inodes: &WasiInodes) {
2336 self.create_std_dev_inner(
2337 inodes,
2338 Box::<Stderr>::default(),
2339 "stderr",
2340 __WASI_STDERR_FILENO,
2341 STDERR_DEFAULT_RIGHTS,
2342 Fdflags::APPEND,
2343 FS_STDERR_INO,
2344 );
2345 }
2346
2347 pub(crate) fn create_rootfd(&self) -> Result<(), String> {
2348 let all_rights = ALL_RIGHTS;
2350 let root_rights = all_rights
2353 ;
2369 let fd = self
2370 .create_fd(
2371 root_rights,
2372 root_rights,
2373 Fdflags::empty(),
2374 Fdflagsext::empty(),
2375 Fd::READ,
2376 self.root_inode.clone(),
2377 )
2378 .map_err(|e| format!("Could not create root fd: {e}"))?;
2379 self.preopen_fds.write().unwrap().push(fd);
2380 Ok(())
2381 }
2382
2383 pub(crate) fn create_preopens(
2384 &self,
2385 inodes: &WasiInodes,
2386 ignore_duplicates: bool,
2387 ) -> Result<(), String> {
2388 for preopen_name in self.init_vfs_preopens.iter() {
2389 let kind = Kind::Dir {
2390 parent: self.root_inode.downgrade(),
2391 path: PathBuf::from(preopen_name),
2392 entries: Default::default(),
2393 };
2394 let rights = Rights::FD_ADVISE
2395 | Rights::FD_TELL
2396 | Rights::FD_SEEK
2397 | Rights::FD_READ
2398 | Rights::PATH_OPEN
2399 | Rights::FD_READDIR
2400 | Rights::PATH_READLINK
2401 | Rights::PATH_FILESTAT_GET
2402 | Rights::FD_FILESTAT_GET
2403 | Rights::PATH_LINK_SOURCE
2404 | Rights::PATH_RENAME_SOURCE
2405 | Rights::POLL_FD_READWRITE
2406 | Rights::SOCK_SHUTDOWN;
2407 let inode = self
2408 .create_inode(inodes, kind, true, preopen_name.clone())
2409 .map_err(|e| {
2410 format!(
2411 "Failed to create inode for preopened dir (name `{preopen_name}`): WASI error code: {e}",
2412 )
2413 })?;
2414 let fd_flags = Fd::READ;
2415 let fd = self
2416 .create_fd(
2417 rights,
2418 rights,
2419 Fdflags::empty(),
2420 Fdflagsext::empty(),
2421 fd_flags,
2422 inode.clone(),
2423 )
2424 .map_err(|e| format!("Could not open fd for file {preopen_name:?}: {e}"))?;
2425 {
2426 let mut guard = self.root_inode.write();
2427 if let Kind::Root { entries } = guard.deref_mut() {
2428 let existing_entry = entries.insert(preopen_name.clone(), inode);
2429 if existing_entry.is_some() && !ignore_duplicates {
2430 return Err(format!("Found duplicate entry for alias `{preopen_name}`"));
2431 }
2432 }
2433 }
2434 self.preopen_fds.write().unwrap().push(fd);
2435 }
2436
2437 for PreopenedDir {
2438 path,
2439 alias,
2440 read,
2441 write,
2442 create,
2443 } in self.init_preopens.iter()
2444 {
2445 debug!(
2446 "Attempting to preopen {} with alias {:?}",
2447 &path.to_string_lossy(),
2448 &alias
2449 );
2450 let cur_dir_metadata = self
2451 .root_fs
2452 .metadata(path)
2453 .map_err(|e| format!("Could not get metadata for file {path:?}: {e}"))?;
2454
2455 let kind = if cur_dir_metadata.is_dir() {
2456 Kind::Dir {
2457 parent: self.root_inode.downgrade(),
2458 path: path.clone(),
2459 entries: Default::default(),
2460 }
2461 } else {
2462 return Err(format!(
2463 "WASI only supports pre-opened directories right now; found \"{}\"",
2464 path.to_string_lossy()
2465 ));
2466 };
2467
2468 let rights = {
2469 let mut rights = Rights::FD_ADVISE | Rights::FD_TELL | Rights::FD_SEEK;
2471 if *read {
2472 rights |= Rights::FD_READ
2473 | Rights::PATH_OPEN
2474 | Rights::FD_READDIR
2475 | Rights::PATH_READLINK
2476 | Rights::PATH_FILESTAT_GET
2477 | Rights::FD_FILESTAT_GET
2478 | Rights::PATH_LINK_SOURCE
2479 | Rights::PATH_RENAME_SOURCE
2480 | Rights::POLL_FD_READWRITE
2481 | Rights::SOCK_SHUTDOWN;
2482 }
2483 if *write {
2484 rights |= Rights::FD_DATASYNC
2485 | Rights::FD_FDSTAT_SET_FLAGS
2486 | Rights::FD_WRITE
2487 | Rights::FD_SYNC
2488 | Rights::FD_ALLOCATE
2489 | Rights::PATH_OPEN
2490 | Rights::PATH_RENAME_TARGET
2491 | Rights::PATH_FILESTAT_SET_SIZE
2492 | Rights::PATH_FILESTAT_SET_TIMES
2493 | Rights::FD_FILESTAT_SET_SIZE
2494 | Rights::FD_FILESTAT_SET_TIMES
2495 | Rights::PATH_REMOVE_DIRECTORY
2496 | Rights::PATH_UNLINK_FILE
2497 | Rights::POLL_FD_READWRITE
2498 | Rights::SOCK_SHUTDOWN;
2499 }
2500 if *create {
2501 rights |= Rights::PATH_CREATE_DIRECTORY
2502 | Rights::PATH_CREATE_FILE
2503 | Rights::PATH_LINK_TARGET
2504 | Rights::PATH_OPEN
2505 | Rights::PATH_RENAME_TARGET
2506 | Rights::PATH_SYMLINK;
2507 }
2508
2509 rights
2510 };
2511 let inode = if let Some(alias) = &alias {
2512 self.create_inode(inodes, kind, true, alias.clone())
2513 } else {
2514 self.create_inode(inodes, kind, true, path.to_string_lossy().into_owned())
2515 }
2516 .map_err(|e| {
2517 format!("Failed to create inode for preopened dir: WASI error code: {e}")
2518 })?;
2519 let fd_flags = {
2520 let mut fd_flags = 0;
2521 if *read {
2522 fd_flags |= Fd::READ;
2523 }
2524 if *write {
2525 fd_flags |= Fd::WRITE | Fd::APPEND | Fd::TRUNCATE;
2527 }
2528 if *create {
2529 fd_flags |= Fd::CREATE;
2530 }
2531 fd_flags
2532 };
2533 let fd = self
2534 .create_fd(
2535 rights,
2536 rights,
2537 Fdflags::empty(),
2538 Fdflagsext::empty(),
2539 fd_flags,
2540 inode.clone(),
2541 )
2542 .map_err(|e| format!("Could not open fd for file {path:?}: {e}"))?;
2543 {
2544 let mut guard = self.root_inode.write();
2545 if let Kind::Root { entries } = guard.deref_mut() {
2546 let key = if let Some(alias) = &alias {
2547 alias.clone()
2548 } else {
2549 path.to_string_lossy().into_owned()
2550 };
2551 let existing_entry = entries.insert(key.clone(), inode);
2552 if existing_entry.is_some() && !ignore_duplicates {
2553 return Err(format!("Found duplicate entry for alias `{key}`"));
2554 }
2555 }
2556 }
2557 self.preopen_fds.write().unwrap().push(fd);
2558 }
2559
2560 Ok(())
2561 }
2562
2563 #[allow(clippy::too_many_arguments)]
2564 pub(crate) fn create_std_dev_inner(
2565 &self,
2566 inodes: &WasiInodes,
2567 handle: Box<dyn VirtualFile + Send + Sync + 'static>,
2568 name: &'static str,
2569 raw_fd: WasiFd,
2570 rights: Rights,
2571 fd_flags: Fdflags,
2572 st_ino: Inode,
2573 ) {
2574 let inode = {
2575 let stat = Filestat {
2576 st_filetype: Filetype::CharacterDevice,
2577 st_ino: st_ino.as_u64(),
2578 ..Filestat::default()
2579 };
2580 let kind = Kind::File {
2581 fd: Some(raw_fd),
2582 handle: Some(Arc::new(RwLock::new(handle))),
2583 path: "".into(),
2584 };
2585 inodes.add_inode_val(InodeVal {
2586 stat: RwLock::new(stat),
2587 is_preopened: true,
2588 name: RwLock::new(name.to_string().into()),
2589 kind: RwLock::new(kind),
2590 })
2591 };
2592 self.fd_map.write().unwrap().insert(
2593 false,
2594 raw_fd,
2595 Fd {
2596 inner: FdInner {
2597 rights,
2598 rights_inheriting: Rights::empty(),
2599 flags: fd_flags,
2600 offset: Arc::new(AtomicU64::new(0)),
2601 fd_flags: Fdflagsext::empty(),
2602 },
2603 open_flags: 0,
2605 inode,
2606 is_stdio: true,
2607 },
2608 );
2609 }
2610
2611 pub fn get_stat_for_kind(&self, kind: &Kind) -> Result<Filestat, Errno> {
2612 let md = match kind {
2613 Kind::File { handle, path, .. } => match handle {
2614 Some(wf) => {
2615 let wf = wf.read().unwrap();
2616 return Ok(Filestat {
2617 st_filetype: Filetype::RegularFile,
2618 st_ino: Inode::from_path(path.to_string_lossy().as_ref()).as_u64(),
2619 st_size: wf.size(),
2620 st_atim: wf.last_accessed(),
2621 st_mtim: wf.last_modified(),
2622 st_ctim: wf.created_time(),
2623
2624 ..Filestat::default()
2625 });
2626 }
2627 None => self
2628 .root_fs
2629 .metadata(path)
2630 .map_err(fs_error_into_wasi_err)?,
2631 },
2632 Kind::Dir { path, .. } => self
2633 .root_fs
2634 .metadata(path)
2635 .map_err(fs_error_into_wasi_err)?,
2636 Kind::Symlink {
2637 path_to_symlink,
2638 relative_path,
2639 ..
2640 } => {
2641 let symlink_path = PosixPath::new("/")
2642 .join(&PosixPath::from_path(path_to_symlink))
2643 .into_path_buf();
2644
2645 match self.root_fs.symlink_metadata(&symlink_path) {
2646 Ok(md) => md,
2647 Err(FsError::EntryNotFound)
2648 if self.ephemeral_symlink_at(&symlink_path).is_some() =>
2649 {
2650 return Ok(Filestat {
2651 st_filetype: Filetype::SymbolicLink,
2652 st_size: relative_path.as_os_str().len() as u64,
2653 ..Filestat::default()
2654 });
2655 }
2656 Err(err) => return Err(fs_error_into_wasi_err(err)),
2657 }
2658 }
2659 _ => return Err(Errno::Io),
2660 };
2661 Ok(Filestat {
2662 st_filetype: virtual_file_type_to_wasi_file_type(md.file_type()),
2663 st_size: md.len(),
2664 st_atim: md.accessed(),
2665 st_mtim: md.modified(),
2666 st_ctim: md.created(),
2667 ..Filestat::default()
2668 })
2669 }
2670
2671 pub(crate) fn close_fd_and_capture_flush(&self, fd: WasiFd) -> CloseFdOutcome {
2676 let mut fd_map = self.fd_map.write().unwrap();
2677 Self::close_fd_locked(&mut fd_map, fd)
2678 }
2679
2680 fn close_fd_locked(fd_map: &mut FdList, fd: WasiFd) -> CloseFdOutcome {
2682 let Some(fd_ref) = fd_map.get(fd) else {
2683 trace!(%fd, "closing file descriptor failed - {}", Errno::Badf);
2684 return CloseFdOutcome::not_found();
2685 };
2686
2687 if !fd_ref.is_stdio && fd_ref.inode.is_preopened {
2688 return CloseFdOutcome {
2689 skipped_preopen: true,
2690 removed: false,
2691 flush_target: None,
2692 };
2693 }
2694
2695 let flush_target = Self::file_flush_target(&fd_ref.inode);
2696
2697 match fd_map.remove(fd) {
2698 Some(fd_ref) => {
2699 let inode = fd_ref.inode.ino().as_u64();
2700 let ref_cnt = fd_ref.inode.ref_cnt();
2701 if ref_cnt == 1 {
2702 trace!(%fd, %inode, %ref_cnt, "closing file descriptor");
2703 } else {
2704 trace!(%fd, %inode, %ref_cnt, "weakening file descriptor");
2705 }
2706 }
2707 None => {
2708 trace!(%fd, "closing file descriptor failed - {}", Errno::Badf);
2709 return CloseFdOutcome::not_found();
2710 }
2711 }
2712
2713 CloseFdOutcome {
2714 skipped_preopen: false,
2715 removed: true,
2716 flush_target,
2717 }
2718 }
2719
2720 pub(crate) async fn flush_file_best_effort(file: VirtualFileLock) {
2721 let result = FlushPoller { file }.await;
2722 match result {
2723 Ok(())
2724 | Err(Errno::Isdir)
2725 | Err(Errno::Io)
2726 | Err(Errno::Access)
2727 | Err(Errno::Inval) => {}
2729 Err(err) => trace!("flush during bulk close failed - {}", err),
2730 }
2731 }
2732
2733 fn file_flush_target(inode: &InodeGuard) -> Option<VirtualFileLock> {
2734 let guard = inode.read();
2735 match guard.deref() {
2736 Kind::File {
2737 handle: Some(file), ..
2738 } => Some(file.clone()),
2739 _ => None,
2740 }
2741 }
2742
2743 pub(crate) fn close_fd(&self, fd: WasiFd) -> Result<(), Errno> {
2745 let _ = self.close_fd_and_capture_flush(fd);
2746 Ok(())
2747 }
2748}
2749
2750impl std::fmt::Debug for WasiFs {
2751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2752 if let Ok(guard) = self.current_dir.try_lock() {
2753 write!(f, "current_dir={} ", guard.as_str())?;
2754 } else {
2755 write!(f, "current_dir=(locked) ")?;
2756 }
2757 if let Ok(guard) = self.fd_map.read() {
2758 write!(
2759 f,
2760 "next_fd={} max_fd={:?} ",
2761 guard.next_free_fd(),
2762 guard.last_fd()
2763 )?;
2764 } else {
2765 write!(f, "next_fd=(locked) max_fd=(locked) ")?;
2766 }
2767 write!(f, "{:?}", self.root_fs)
2768 }
2769}
2770
2771pub fn default_fs_backing() -> Arc<dyn virtual_fs::FileSystem + Send + Sync> {
2773 cfg_if::cfg_if! {
2774 if #[cfg(feature = "host-fs")] {
2775 Arc::new(virtual_fs::host_fs::FileSystem::new(tokio::runtime::Handle::current(), "/").unwrap())
2776 } else if #[cfg(not(feature = "host-fs"))] {
2777 Arc::<virtual_fs::mem_fs::FileSystem>::default()
2778 } else {
2779 Arc::<FallbackFileSystem>::default()
2780 }
2781 }
2782}
2783
2784#[derive(Debug, Default)]
2785pub struct FallbackFileSystem;
2786
2787impl FallbackFileSystem {
2788 fn fail() -> ! {
2789 panic!(
2790 "No filesystem set for wasmer-wasi, please enable either the `host-fs` or `mem-fs` feature or set your custom filesystem with `WasiEnvBuilder::set_fs`"
2791 );
2792 }
2793}
2794
2795impl FileSystem for FallbackFileSystem {
2796 fn readlink(&self, _path: &Path) -> virtual_fs::Result<PathBuf> {
2797 Self::fail()
2798 }
2799 fn read_dir(&self, _path: &Path) -> Result<virtual_fs::ReadDir, FsError> {
2800 Self::fail();
2801 }
2802 fn create_dir(&self, _path: &Path) -> Result<(), FsError> {
2803 Self::fail();
2804 }
2805 fn remove_dir(&self, _path: &Path) -> Result<(), FsError> {
2806 Self::fail();
2807 }
2808 fn rename<'a>(&'a self, _from: &Path, _to: &Path) -> BoxFuture<'a, Result<(), FsError>> {
2809 Self::fail();
2810 }
2811 fn metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2812 Self::fail();
2813 }
2814 fn symlink_metadata(&self, _path: &Path) -> Result<virtual_fs::Metadata, FsError> {
2815 Self::fail();
2816 }
2817 fn remove_file(&self, _path: &Path) -> Result<(), FsError> {
2818 Self::fail();
2819 }
2820 fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
2821 Self::fail();
2822 }
2823}
2824
2825pub fn virtual_file_type_to_wasi_file_type(file_type: virtual_fs::FileType) -> Filetype {
2826 if file_type.is_dir() {
2828 Filetype::Directory
2829 } else if file_type.is_file() {
2830 Filetype::RegularFile
2831 } else if file_type.is_symlink() {
2832 Filetype::SymbolicLink
2833 } else {
2834 Filetype::Unknown
2835 }
2836}
2837
2838pub fn fs_error_from_wasi_err(err: Errno) -> FsError {
2839 match err {
2840 Errno::Badf => FsError::InvalidFd,
2841 Errno::Exist => FsError::AlreadyExists,
2842 Errno::Io => FsError::IOError,
2843 Errno::Addrinuse => FsError::AddressInUse,
2844 Errno::Addrnotavail => FsError::AddressNotAvailable,
2845 Errno::Pipe => FsError::BrokenPipe,
2846 Errno::Connaborted => FsError::ConnectionAborted,
2847 Errno::Connrefused => FsError::ConnectionRefused,
2848 Errno::Connreset => FsError::ConnectionReset,
2849 Errno::Intr => FsError::Interrupted,
2850 Errno::Inval => FsError::InvalidInput,
2851 Errno::Notconn => FsError::NotConnected,
2852 Errno::Nodev => FsError::NoDevice,
2853 Errno::Noent => FsError::EntryNotFound,
2854 Errno::Perm => FsError::PermissionDenied,
2855 Errno::Timedout => FsError::TimedOut,
2856 Errno::Proto => FsError::UnexpectedEof,
2857 Errno::Again => FsError::WouldBlock,
2858 Errno::Nospc => FsError::WriteZero,
2859 Errno::Notempty => FsError::DirectoryNotEmpty,
2860 _ => FsError::UnknownError,
2861 }
2862}
2863
2864pub fn fs_error_into_wasi_err(fs_error: FsError) -> Errno {
2865 match fs_error {
2866 FsError::AlreadyExists => Errno::Exist,
2867 FsError::AddressInUse => Errno::Addrinuse,
2868 FsError::AddressNotAvailable => Errno::Addrnotavail,
2869 FsError::BaseNotDirectory => Errno::Notdir,
2870 FsError::BrokenPipe => Errno::Pipe,
2871 FsError::ConnectionAborted => Errno::Connaborted,
2872 FsError::ConnectionRefused => Errno::Connrefused,
2873 FsError::ConnectionReset => Errno::Connreset,
2874 FsError::Interrupted => Errno::Intr,
2875 FsError::InvalidData => Errno::Io,
2876 FsError::InvalidFd => Errno::Badf,
2877 FsError::InvalidInput => Errno::Inval,
2878 FsError::IOError => Errno::Io,
2879 FsError::NoDevice => Errno::Nodev,
2880 FsError::NotAFile => Errno::Inval,
2881 FsError::NotConnected => Errno::Notconn,
2882 FsError::EntryNotFound => Errno::Noent,
2883 FsError::PermissionDenied => Errno::Perm,
2884 FsError::TimedOut => Errno::Timedout,
2885 FsError::UnexpectedEof => Errno::Proto,
2886 FsError::WouldBlock => Errno::Again,
2887 FsError::WriteZero => Errno::Nospc,
2888 FsError::DirectoryNotEmpty => Errno::Notempty,
2889 FsError::StorageFull => Errno::Overflow,
2890 FsError::Lock | FsError::UnknownError => Errno::Io,
2891 FsError::Unsupported => Errno::Notsup,
2892 }
2893}
2894
2895#[cfg(test)]
2896mod tests {
2897 use super::*;
2898 use once_cell::sync::OnceCell;
2899 use tempfile::tempdir;
2900 use virtual_fs::{RootFileSystemBuilder, TmpFileSystem};
2901 use wasmer::Engine;
2902 use wasmer_config::package::PackageId;
2903
2904 use crate::WasiEnvBuilder;
2905 use crate::bin_factory::{BinaryPackage, BinaryPackageMount, BinaryPackageMounts};
2906
2907 fn webc_symlink_fs() -> virtual_fs::WebcVolumeFileSystem {
2908 let timestamps = webc::v3::Timestamps::default();
2909 let dir = webc::v3::write::Directory::new(
2910 std::collections::BTreeMap::from_iter([
2911 (
2912 webc::PathSegment::parse("target.txt").unwrap(),
2913 webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
2914 b"target", timestamps,
2915 )),
2916 ),
2917 (
2918 webc::PathSegment::parse("link").unwrap(),
2919 webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
2920 "target.txt",
2921 timestamps,
2922 )),
2923 ),
2924 ]),
2925 timestamps,
2926 );
2927 let manifest = webc::metadata::Manifest::default();
2928 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
2929 .write_manifest(&manifest)
2930 .unwrap()
2931 .write_atoms(std::collections::BTreeMap::new())
2932 .unwrap();
2933 writer.write_volume("atom", dir).unwrap();
2934 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
2935 let container = wasmer_package::utils::from_bytes(webc).unwrap();
2936 let volume = container.volumes()["atom"].clone();
2937
2938 virtual_fs::WebcVolumeFileSystem::new(volume)
2939 }
2940
2941 #[tokio::test]
2942 async fn test_relative_path_to_absolute() {
2943 let inodes = WasiInodes::new();
2944 let fs_backing =
2945 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
2946 let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
2947
2948 assert_eq!(
2950 wasi_fs.relative_path_to_absolute("/foo/bar".to_string()),
2951 "/foo/bar"
2952 );
2953 assert_eq!(wasi_fs.relative_path_to_absolute("/".to_string()), "/");
2954
2955 assert_eq!(
2957 wasi_fs.relative_path_to_absolute("//foo//bar//".to_string()),
2958 "//foo//bar//"
2959 );
2960 assert_eq!(
2961 wasi_fs.relative_path_to_absolute("/a/b/./c".to_string()),
2962 "/a/b/./c"
2963 );
2964 assert_eq!(
2965 wasi_fs.relative_path_to_absolute("/a/b/../c".to_string()),
2966 "/a/b/../c"
2967 );
2968
2969 assert_eq!(
2971 wasi_fs.relative_path_to_absolute("foo/bar".to_string()),
2972 "/foo/bar"
2973 );
2974 assert_eq!(wasi_fs.relative_path_to_absolute("foo".to_string()), "/foo");
2975
2976 wasi_fs.set_current_dir("/home/user");
2978 assert_eq!(
2979 wasi_fs.relative_path_to_absolute("file.txt".to_string()),
2980 "/home/user/file.txt"
2981 );
2982 assert_eq!(
2983 wasi_fs.relative_path_to_absolute("dir/file.txt".to_string()),
2984 "/home/user/dir/file.txt"
2985 );
2986
2987 wasi_fs.set_current_dir("/a/b/c");
2989 assert_eq!(
2990 wasi_fs.relative_path_to_absolute("./file.txt".to_string()),
2991 "/a/b/c/./file.txt"
2992 );
2993 assert_eq!(
2994 wasi_fs.relative_path_to_absolute("../file.txt".to_string()),
2995 "/a/b/c/../file.txt"
2996 );
2997 assert_eq!(
2998 wasi_fs.relative_path_to_absolute("../../file.txt".to_string()),
2999 "/a/b/c/../../file.txt"
3000 );
3001
3002 assert_eq!(
3004 wasi_fs.relative_path_to_absolute(".".to_string()),
3005 "/a/b/c/."
3006 );
3007 assert_eq!(
3008 wasi_fs.relative_path_to_absolute("..".to_string()),
3009 "/a/b/c/.."
3010 );
3011 assert_eq!(wasi_fs.relative_path_to_absolute("".to_string()), "/a/b/c/");
3012
3013 wasi_fs.set_current_dir("/home/user/");
3015 assert_eq!(
3016 wasi_fs.relative_path_to_absolute("file.txt".to_string()),
3017 "/home/user/file.txt"
3018 );
3019
3020 wasi_fs.set_current_dir("/home/user");
3022 assert_eq!(
3023 wasi_fs.relative_path_to_absolute("file.txt".to_string()),
3024 "/home/user/file.txt"
3025 );
3026 }
3027
3028 #[cfg(feature = "host-fs")]
3029 #[tokio::test]
3030 async fn mapped_preopen_inode_paths_should_stay_in_guest_space() {
3031 let root_dir = tempdir().unwrap();
3032 let hamlet_dir = root_dir.path().join("hamlet");
3033 std::fs::create_dir_all(&hamlet_dir).unwrap();
3034
3035 let host_fs = virtual_fs::host_fs::FileSystem::new(
3036 tokio::runtime::Handle::current(),
3037 root_dir.path(),
3038 )
3039 .unwrap();
3040
3041 let init = WasiEnvBuilder::new("test_prog")
3042 .engine(Engine::default())
3043 .fs(Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>)
3044 .map_dir("hamlet", "/hamlet")
3045 .unwrap()
3046 .build_init()
3047 .unwrap();
3048
3049 let preopen_inode = {
3050 let guard = init.state.fs.root_inode.read();
3051 let Kind::Root { entries } = guard.deref() else {
3052 panic!("expected root inode");
3053 };
3054 entries.get("hamlet").unwrap().clone()
3055 };
3056 let guard = preopen_inode.read();
3057
3058 let Kind::Dir { path, .. } = guard.deref() else {
3059 panic!("expected preopen inode to be a directory");
3060 };
3061
3062 assert_eq!(path, std::path::Path::new("/hamlet"));
3063 }
3064
3065 #[cfg(all(unix, feature = "host-fs", feature = "sys"))]
3066 #[tokio::test]
3067 async fn backing_absolute_host_symlink_targets_stay_within_guest_mount() {
3068 let root_dir = tempfile::Builder::new()
3069 .prefix("wasix-backing-symlink")
3070 .tempdir_in("/tmp")
3071 .unwrap();
3072 let dir1 = root_dir.path().join("dir1");
3073 let dir2 = root_dir.path().join("dir2");
3074 std::fs::create_dir_all(&dir1).unwrap();
3075 std::fs::write(dir1.join("file1"), b"hello").unwrap();
3076 std::os::unix::fs::symlink(&dir1, &dir2).unwrap();
3077
3078 let host_fs = virtual_fs::host_fs::FileSystem::new(
3079 tokio::runtime::Handle::current(),
3080 root_dir.path(),
3081 )
3082 .unwrap();
3083 let mount_fs = virtual_fs::MountFileSystem::new();
3084 mount_fs
3085 .mount(
3086 Path::new("/"),
3087 Arc::new(RootFileSystemBuilder::default().build_tmp()),
3088 )
3089 .unwrap();
3090 mount_fs
3091 .mount(
3092 Path::new("/host"),
3093 Arc::new(host_fs) as Arc<dyn FileSystem + Send + Sync>,
3094 )
3095 .unwrap();
3096
3097 let inodes = WasiInodes::new();
3098 let fs_backing = WasiFsRoot::from_mount_fs(mount_fs);
3099 let wasi_fs =
3100 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3101
3102 let literal_link = wasi_fs
3103 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2", false)
3104 .unwrap();
3105 assert!(matches!(
3106 literal_link.read().deref(),
3107 Kind::Symlink {
3108 symlink_kind: SymlinkKind::Backing,
3109 relative_path,
3110 ..
3111 } if relative_path == Path::new("/dir1")
3112 ));
3113
3114 let followed_dir = wasi_fs
3115 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2", true)
3116 .unwrap();
3117 let followed_dir_path = {
3118 let guard = followed_dir.read();
3119 let Kind::Dir { path, .. } = guard.deref() else {
3120 panic!("expected followed backing symlink to resolve to a directory");
3121 };
3122 assert_eq!(path, Path::new("/host/dir1"));
3123 path.clone()
3124 };
3125 let mut entries = wasi_fs.root_fs.read_dir(&followed_dir_path).unwrap();
3126 assert!(entries.any(|entry| entry.unwrap().path() == Path::new("/host/dir1/file1")));
3127
3128 let child = wasi_fs
3129 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/host/dir2/file1", true)
3130 .unwrap();
3131 assert!(matches!(
3132 child.read().deref(),
3133 Kind::File { path, .. } if path == Path::new("/host/dir1/file1")
3134 ));
3135 }
3136
3137 #[tokio::test]
3138 async fn dot_mapped_preopen_uses_guest_current_dir() {
3139 let init = WasiEnvBuilder::new("test_prog")
3140 .engine(Engine::default())
3141 .current_dir("/work")
3142 .map_dir(".", "/work")
3143 .unwrap()
3144 .build_init()
3145 .unwrap();
3146
3147 let preopen_inode = {
3148 let guard = init.state.fs.root_inode.read();
3149 let Kind::Root { entries } = guard.deref() else {
3150 panic!("expected root inode");
3151 };
3152 entries.get(".").unwrap().clone()
3153 };
3154 let guard = preopen_inode.read();
3155
3156 let Kind::Dir { path, .. } = guard.deref() else {
3157 panic!("expected preopen inode to be a directory");
3158 };
3159
3160 assert_eq!(path, std::path::Path::new("/work"));
3161 }
3162
3163 #[tokio::test]
3164 async fn symlinked_directory_components_resolve_to_target_entries() {
3165 let inodes = WasiInodes::new();
3166 let fs_backing =
3167 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3168 let wasi_fs =
3169 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3170 let root = &wasi_fs.root_fs;
3171
3172 root.create_dir(Path::new("/orig")).unwrap();
3173 root.new_open_options()
3174 .create(true)
3175 .write(true)
3176 .open(Path::new("/orig/child.txt"))
3177 .unwrap();
3178 root.create_symlink(Path::new("/orig"), Path::new("/linked"))
3179 .unwrap();
3180
3181 let literal_link = wasi_fs
3182 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked", false)
3183 .unwrap();
3184 assert!(matches!(
3185 literal_link.read().deref(),
3186 Kind::Symlink {
3187 relative_path,
3188 ..
3189 } if relative_path == Path::new("/orig")
3190 ));
3191
3192 let followed_dir = wasi_fs
3193 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked", true)
3194 .unwrap();
3195 assert!(matches!(
3196 followed_dir.read().deref(),
3197 Kind::Dir { path, .. } if path == Path::new("/orig")
3198 ));
3199
3200 let child = wasi_fs
3201 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked/child.txt", true)
3202 .unwrap();
3203 assert!(matches!(
3204 child.read().deref(),
3205 Kind::File { path, .. } if path == Path::new("/orig/child.txt")
3206 ));
3207
3208 let child_without_final_follow = wasi_fs
3209 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/linked/child.txt", false)
3210 .unwrap();
3211 assert!(matches!(
3212 child_without_final_follow.read().deref(),
3213 Kind::File { path, .. } if path == Path::new("/orig/child.txt")
3214 ));
3215 }
3216
3217 #[tokio::test]
3218 async fn webc_backing_symlink_resolves_to_target_entry() {
3219 let inodes = WasiInodes::new();
3220 let fs_backing = WasiFsRoot::from_filesystem(Arc::new(webc_symlink_fs()));
3221 let wasi_fs =
3222 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3223
3224 let literal_link = wasi_fs
3225 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", false)
3226 .unwrap();
3227 assert!(matches!(
3228 literal_link.read().deref(),
3229 Kind::Symlink {
3230 relative_path,
3231 ..
3232 } if relative_path == Path::new("target.txt")
3233 ));
3234
3235 let followed_file = wasi_fs
3236 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", true)
3237 .unwrap();
3238 assert!(matches!(
3239 followed_file.read().deref(),
3240 Kind::File { path, .. } if path == Path::new("/target.txt")
3241 ));
3242 }
3243
3244 #[tokio::test]
3245 async fn path_resolution_preserves_posix_directory_component_rules() {
3246 let inodes = WasiInodes::new();
3247 let fs_backing =
3248 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3249 let wasi_fs =
3250 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3251 let root = &wasi_fs.root_fs;
3252
3253 root.create_dir(Path::new("/dir")).unwrap();
3254 root.new_open_options()
3255 .create(true)
3256 .write(true)
3257 .open(Path::new("/file"))
3258 .unwrap();
3259 root.create_symlink(Path::new("/dir"), Path::new("/dir-link"))
3260 .unwrap();
3261 root.create_symlink(Path::new("/file"), Path::new("/file-link"))
3262 .unwrap();
3263
3264 let empty_path = wasi_fs
3265 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "", true)
3266 .unwrap_err();
3267 assert_eq!(empty_path, Errno::Noent);
3268
3269 let (single_component_parent, single_component_name) = wasi_fs
3270 .get_parent_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, Path::new("new-file"), true)
3271 .unwrap();
3272 assert_eq!(single_component_name, "new-file");
3273 assert!(matches!(
3274 single_component_parent.read().deref(),
3275 Kind::Root { .. }
3276 ));
3277
3278 let root_parent = wasi_fs
3279 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/..", true)
3280 .unwrap();
3281 assert!(matches!(root_parent.read().deref(), Kind::Root { .. }));
3282
3283 let escaped_symlink_target = wasi_fs
3284 .resolve_symlink_target_path(
3285 SymlinkKind::Virtual,
3286 Path::new("fs_sandbox_symlink.dir/link"),
3287 Path::new("../../README.md"),
3288 )
3289 .unwrap_err();
3290 assert_eq!(escaped_symlink_target, Errno::Perm);
3291
3292 let (_, contained_symlink_target) = wasi_fs
3293 .resolve_symlink_target_path(
3294 SymlinkKind::Virtual,
3295 Path::new("fs_sandbox_symlink.dir/link"),
3296 Path::new("../README.md"),
3297 )
3298 .unwrap();
3299 assert_eq!(contained_symlink_target, Path::new("README.md"));
3300
3301 let (_, sibling_preopen_symlink_target) = wasi_fs
3302 .resolve_symlink_target_path(
3303 SymlinkKind::Virtual,
3304 Path::new("temp/act3"),
3305 Path::new("../hamlet/act3"),
3306 )
3307 .unwrap();
3308 assert_eq!(sibling_preopen_symlink_target, Path::new("hamlet/act3"));
3309
3310 let escaped_sibling_preopen_symlink_target = wasi_fs
3311 .resolve_symlink_target_path(
3312 SymlinkKind::Virtual,
3313 Path::new("temp/act3"),
3314 Path::new("../../outside"),
3315 )
3316 .unwrap_err();
3317 assert_eq!(escaped_sibling_preopen_symlink_target, Errno::Perm);
3318
3319 root.create_dir(Path::new("/outerdir")).unwrap();
3320 root.create_dir(Path::new("/outerdir/dest")).unwrap();
3321 root.new_open_options()
3322 .create(true)
3323 .write(true)
3324 .open(Path::new("/outerdir/evil"))
3325 .unwrap();
3326 let dest_dir = wasi_fs
3327 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/outerdir/dest", true)
3328 .unwrap();
3329 let current_link = wasi_fs.create_inode_with_default_stat(
3330 &inodes,
3331 Kind::Symlink {
3332 symlink_kind: SymlinkKind::Virtual,
3333 path_to_symlink: PathBuf::from("outerdir/dest/current"),
3334 relative_path: PathBuf::from("."),
3335 },
3336 false,
3337 Cow::Borrowed("current"),
3338 );
3339 let parent_link = wasi_fs.create_inode_with_default_stat(
3340 &inodes,
3341 Kind::Symlink {
3342 symlink_kind: SymlinkKind::Virtual,
3343 path_to_symlink: PathBuf::from("outerdir/dest/parent"),
3344 relative_path: PathBuf::from("current/.."),
3345 },
3346 false,
3347 Cow::Borrowed("parent"),
3348 );
3349 {
3350 let mut guard = dest_dir.write();
3351 let Kind::Dir { entries, .. } = guard.deref_mut() else {
3352 panic!("expected destination to be a directory");
3353 };
3354 entries.insert("current".to_string(), current_link);
3355 entries.insert("parent".to_string(), parent_link);
3356 }
3357
3358 let parent_symlink_target = wasi_fs
3359 .get_inode_at_path(
3360 &inodes,
3361 crate::VIRTUAL_ROOT_FD,
3362 "/outerdir/dest/parent/evil",
3363 true,
3364 )
3365 .unwrap();
3366 assert!(matches!(
3367 parent_symlink_target.read().deref(),
3368 Kind::File { path, .. } if path == Path::new("/outerdir/evil")
3369 ));
3370
3371 let file_dot = wasi_fs
3372 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file/.", true)
3373 .unwrap_err();
3374 assert_eq!(file_dot, Errno::Notdir);
3375
3376 let file_slash = wasi_fs
3377 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file/", true)
3378 .unwrap_err();
3379 assert_eq!(file_slash, Errno::Notdir);
3380
3381 let symlinked_dir_slash = wasi_fs
3382 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/dir-link/", false)
3383 .unwrap();
3384 assert!(matches!(
3385 symlinked_dir_slash.read().deref(),
3386 Kind::Dir { path, .. } if path == Path::new("/dir")
3387 ));
3388
3389 let symlinked_file_slash = wasi_fs
3390 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/file-link/", false)
3391 .unwrap_err();
3392 assert_eq!(symlinked_file_slash, Errno::Notdir);
3393
3394 root.create_symlink(Path::new("/loop"), Path::new("/loop"))
3395 .unwrap();
3396 let symlink_loop = wasi_fs
3397 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/loop", true)
3398 .unwrap_err();
3399 assert_eq!(symlink_loop, Errno::Loop);
3400 }
3401
3402 #[tokio::test]
3403 async fn writable_root_is_preserved_through_root_overlays() {
3404 let base_root = Arc::new(RootFileSystemBuilder::default().build_tmp());
3405 let root = WasiFsRoot::from_filesystem(base_root);
3406 assert!(root.writable_root().is_some());
3407
3408 let lower = Arc::new(TmpFileSystem::new()) as Arc<dyn FileSystem + Send + Sync>;
3409 root.stack_root_filesystem(lower).unwrap();
3410
3411 assert!(root.writable_root().is_some());
3412 }
3413
3414 #[tokio::test]
3415 async fn conditional_union_merges_root_and_non_root_package_mounts_once() {
3416 let inodes = WasiInodes::new();
3417 let fs_backing =
3418 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3419 let wasi_fs = WasiFs::new_init(fs_backing, &inodes, FS_ROOT_INO).unwrap();
3420
3421 let root_layer = TmpFileSystem::new();
3422 root_layer
3423 .new_open_options()
3424 .create(true)
3425 .write(true)
3426 .open(Path::new("/root.txt"))
3427 .unwrap();
3428
3429 let public_mount = TmpFileSystem::new();
3430 public_mount
3431 .new_open_options()
3432 .create(true)
3433 .write(true)
3434 .open(Path::new("/index.html"))
3435 .unwrap();
3436
3437 let pkg = BinaryPackage {
3438 id: PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap()),
3439 package_ids: vec![],
3440 when_cached: None,
3441 entrypoint_cmd: None,
3442 hash: OnceCell::new(),
3443 package_mounts: Some(Arc::new(BinaryPackageMounts {
3444 root_layer: Some(Arc::new(root_layer)),
3445 mounts: vec![BinaryPackageMount {
3446 guest_path: PathBuf::from("/public"),
3447 fs: Arc::new(public_mount),
3448 source_path: PathBuf::from("/"),
3449 }],
3450 })),
3451 commands: vec![],
3452 uses: vec![],
3453 file_system_memory_footprint: 0,
3454 additional_host_mapped_directories: vec![],
3455 };
3456
3457 wasi_fs.conditional_union(&pkg).await.unwrap();
3458 assert!(
3459 wasi_fs
3460 .root_fs
3461 .metadata(Path::new("/root.txt"))
3462 .unwrap()
3463 .is_file()
3464 );
3465 assert!(
3466 wasi_fs
3467 .root_fs
3468 .metadata(Path::new("/public/index.html"))
3469 .unwrap()
3470 .is_file()
3471 );
3472
3473 wasi_fs.conditional_union(&pkg).await.unwrap();
3474 assert!(
3475 wasi_fs
3476 .root_fs
3477 .metadata(Path::new("/root.txt"))
3478 .unwrap()
3479 .is_file()
3480 );
3481 assert!(
3482 wasi_fs
3483 .root_fs
3484 .metadata(Path::new("/public/index.html"))
3485 .unwrap()
3486 .is_file()
3487 );
3488 }
3489
3490 #[tokio::test]
3491 async fn ephemeral_symlink_resolves_without_caching_a_directory_entry() {
3492 let inodes = WasiInodes::new();
3495 let fs_backing =
3496 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3497 let wasi_fs =
3498 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3499
3500 wasi_fs.register_ephemeral_symlink(
3501 PathBuf::from("/link"),
3502 PathBuf::from("link"),
3503 PathBuf::from("target.txt"),
3504 );
3505
3506 let link = wasi_fs
3507 .get_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, "/link", false)
3508 .unwrap();
3509 assert!(matches!(
3510 link.read().deref(),
3511 Kind::Symlink {
3512 symlink_kind: SymlinkKind::Virtual,
3513 relative_path,
3514 ..
3515 } if relative_path == Path::new("target.txt")
3516 ));
3517
3518 let (parent_inode, child_name) = wasi_fs
3520 .get_parent_inode_at_path(&inodes, crate::VIRTUAL_ROOT_FD, Path::new("/link"), false)
3521 .unwrap();
3522 assert_eq!(child_name, "link");
3523 match parent_inode.read().deref() {
3524 Kind::Dir { entries, .. } | Kind::Root { entries } => {
3525 assert!(!entries.contains_key("link"));
3526 }
3527 _ => panic!("expected the parent of /link to be a directory"),
3528 }
3529 }
3530
3531 #[tokio::test]
3532 async fn remove_symlink_file_drops_ephemeral_link_without_backing_file() {
3533 let inodes = WasiInodes::new();
3535 let fs_backing =
3536 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3537 let wasi_fs =
3538 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3539
3540 wasi_fs
3541 .root_fs
3542 .new_open_options()
3543 .create(true)
3544 .write(true)
3545 .open(Path::new("/target.txt"))
3546 .unwrap();
3547 wasi_fs.register_ephemeral_symlink(
3548 PathBuf::from("/link"),
3549 PathBuf::from("link"),
3550 PathBuf::from("target.txt"),
3551 );
3552 assert!(wasi_fs.ephemeral_symlink_at(Path::new("/link")).is_some());
3553
3554 assert_eq!(
3555 wasi_fs.remove_symlink_file(Path::new("/link")),
3556 Errno::Success
3557 );
3558 assert!(wasi_fs.ephemeral_symlink_at(Path::new("/link")).is_none());
3559 assert!(
3560 wasi_fs
3561 .root_fs
3562 .metadata(Path::new("/target.txt"))
3563 .unwrap()
3564 .is_file()
3565 );
3566 }
3567
3568 #[tokio::test]
3569 async fn remove_symlink_file_reports_noent_when_nothing_to_remove() {
3570 let inodes = WasiInodes::new();
3571 let fs_backing =
3572 WasiFsRoot::from_filesystem(Arc::new(RootFileSystemBuilder::default().build_tmp()));
3573 let wasi_fs =
3574 WasiFs::new_with_preopen(&inodes, &[], &["/".to_string()], fs_backing).unwrap();
3575
3576 assert_eq!(
3577 wasi_fs.remove_symlink_file(Path::new("/missing")),
3578 Errno::Noent
3579 );
3580 }
3581}