wasmer_wasix/syscalls/wasix/
path_open2.rs

1use super::*;
2use crate::VIRTUAL_ROOT_FD;
3use crate::fs::{FdList, WasiFs};
4use crate::syscalls::*;
5
6/// ### `path_open()`
7/// Open file located at the given path
8/// Inputs:
9/// - `Fd dirfd`
10///     The fd corresponding to the directory that the file is in
11/// - `LookupFlags dirflags`
12///     Flags specifying how the path will be resolved
13/// - `char *path`
14///     The path of the file or directory to open
15/// - `u32 path_len`
16///     The length of the `path` string
17/// - `Oflags o_flags`
18///     How the file will be opened
19/// - `Rights fs_rights_base`
20///     The rights of the created file descriptor
21/// - `Rights fs_rightsinheriting`
22///     The rights of file descriptors derived from the created file descriptor
23/// - `Fdflags fs_flags`
24///     The flags of the file descriptor
25/// Output:
26/// - `Fd* fd`
27///     The new file descriptor
28/// Possible Errors:
29/// - `Errno::Access`, `Errno::Badf`, `Errno::Fault`, `Errno::Fbig?`, `Errno::Inval`, `Errno::Io`, `Errno::Loop`, `Errno::Mfile`, `Errno::Nametoolong?`, `Errno::Nfile`, `Errno::Noent`, `Errno::Notdir`, `Errno::Rofs`, and `Errno::Notcapable`
30#[instrument(level = "trace", skip_all, fields(%dirfd, path = field::Empty, follow_symlinks = field::Empty, ret_fd = field::Empty), ret)]
31pub fn path_open2<M: MemorySize>(
32    mut ctx: FunctionEnvMut<'_, WasiEnv>,
33    dirfd: WasiFd,
34    dirflags: LookupFlags,
35    path: WasmPtr<u8, M>,
36    path_len: M::Offset,
37    o_flags: Oflags,
38    fs_rights_base: Rights,
39    fs_rights_inheriting: Rights,
40    fs_flags: Fdflags,
41    fd_flags: Fdflagsext,
42    fd: WasmPtr<WasiFd, M>,
43) -> Result<Errno, WasiError> {
44    WasiEnv::do_pending_operations(&mut ctx)?;
45
46    if dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0 {
47        Span::current().record("follow_symlinks", true);
48    }
49    let env = ctx.data();
50    let (memory, mut state, mut inodes) =
51        unsafe { env.get_memory_and_wasi_state_and_inodes(&ctx, 0) };
52    /* TODO: find actual upper bound on name size (also this is a path, not a name :think-fish:) */
53    let path_len64: u64 = path_len.into();
54    if path_len64 > 1024u64 * 1024u64 {
55        return Ok(Errno::Nametoolong);
56    }
57
58    if path_len64 == 0 {
59        return Ok(Errno::Noent);
60    }
61
62    // o_flags:
63    // - __WASI_O_CREAT (create if it does not exist)
64    // - __WASI_O_DIRECTORY (fail if not dir)
65    // - __WASI_O_EXCL (fail if file exists)
66    // - __WASI_O_TRUNC (truncate size to 0)
67
68    let path_string = unsafe { get_input_str_ok!(&memory, path, path_len) };
69    Span::current().record("path", path_string.as_str());
70
71    let out_fd = wasi_try_ok!(path_open_internal(
72        ctx.data(),
73        dirfd,
74        dirflags,
75        &path_string,
76        o_flags,
77        fs_rights_base,
78        fs_rights_inheriting,
79        fs_flags,
80        fd_flags,
81        None,
82    )?);
83    let env = ctx.data();
84
85    #[cfg(feature = "journal")]
86    if env.enable_journal {
87        JournalEffector::save_path_open(
88            &mut ctx,
89            out_fd,
90            dirfd,
91            dirflags,
92            path_string,
93            o_flags,
94            fs_rights_base,
95            fs_rights_inheriting,
96            fs_flags,
97            fd_flags,
98        )
99        .map_err(|err| {
100            tracing::error!("failed to save unlink event - {}", err);
101            WasiError::Exit(ExitCode::from(Errno::Fault))
102        })?;
103    }
104
105    let env = ctx.data();
106    let (memory, mut state, mut inodes) =
107        unsafe { env.get_memory_and_wasi_state_and_inodes(&ctx, 0) };
108
109    Span::current().record("ret_fd", out_fd);
110
111    let fd_ref = fd.deref(&memory);
112    wasi_try_mem_ok!(fd_ref.write(out_fd));
113
114    Ok(Errno::Success)
115}
116
117pub(crate) fn path_open_internal(
118    env: &WasiEnv,
119    dirfd: WasiFd,
120    dirflags: LookupFlags,
121    path: &str,
122    o_flags: Oflags,
123    fs_rights_base: Rights,
124    fs_rights_inheriting: Rights,
125    fs_flags: Fdflags,
126    fd_flags: Fdflagsext,
127    with_fd: Option<WasiFd>,
128) -> Result<Result<WasiFd, Errno>, WasiError> {
129    fn implied_fd_rights(has_read_access: bool, has_write_access: bool) -> Rights {
130        let mut rights = Rights::FD_ADVISE | Rights::FD_TELL | Rights::FD_SEEK;
131
132        if has_read_access {
133            rights |= Rights::FD_READ | Rights::FD_FILESTAT_GET;
134        }
135
136        if has_write_access {
137            rights |= Rights::FD_DATASYNC
138                | Rights::FD_FDSTAT_SET_FLAGS
139                | Rights::FD_WRITE
140                | Rights::FD_SYNC
141                | Rights::FD_ALLOCATE
142                | Rights::FD_FILESTAT_GET
143                | Rights::FD_FILESTAT_SET_SIZE
144                | Rights::FD_FILESTAT_SET_TIMES;
145        }
146
147        rights
148    }
149
150    let state = env.state.deref();
151    let inodes = &state.inodes;
152    let follow_symlinks = dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0;
153    let effective_dirfd = if path.starts_with('/') {
154        VIRTUAL_ROOT_FD
155    } else {
156        dirfd
157    };
158    let path_arg = std::path::PathBuf::from(path);
159    let working_dir = match state.fs.get_fd(effective_dirfd) {
160        Ok(fd) => fd,
161        Err(err) => return Ok(Err(err)),
162    };
163    let maybe_inode = state
164        .fs
165        .get_inode_at_path(inodes, effective_dirfd, path, follow_symlinks);
166    let working_dir_rights_inheriting = working_dir.inner.rights_inheriting;
167
168    // ASSUMPTION: open rights apply recursively
169    if !working_dir.inner.rights.contains(Rights::PATH_OPEN) {
170        return Ok(Err(Errno::Access));
171    }
172
173    let mut open_flags = 0;
174    // TODO: traverse rights of dirs properly
175    // COMMENTED OUT: WASI isn't giving appropriate rights here when opening
176    //              TODO: look into this; file a bug report if this is a bug
177    //
178    let has_read_access = fs_rights_base.contains(Rights::FD_READ);
179    let has_write_access = fs_rights_base.contains(Rights::FD_WRITE)
180        || fs_flags.contains(Fdflags::APPEND)
181        || o_flags.contains(Oflags::TRUNC)
182        || o_flags.contains(Oflags::CREATE);
183    let requested_base_rights =
184        fs_rights_base | implied_fd_rights(has_read_access, has_write_access);
185
186    // Maximum rights: whatever the parent fd may delegate
187    // Minimum rights: whatever rights the caller requested or the open mode implies
188    let adjusted_rights = requested_base_rights & working_dir_rights_inheriting;
189    let adjusted_rights_inheriting = fs_rights_inheriting & working_dir_rights_inheriting;
190    let mut open_options = state.fs_new_open_options();
191
192    let target_rights = match maybe_inode {
193        Ok(_) => {
194            let write_permission = adjusted_rights.contains(Rights::FD_WRITE);
195
196            // append, truncate, and create all require the permission to write
197            let (append_permission, truncate_permission, create_permission) = if write_permission {
198                (
199                    fs_flags.contains(Fdflags::APPEND),
200                    o_flags.contains(Oflags::TRUNC),
201                    o_flags.contains(Oflags::CREATE),
202                )
203            } else {
204                (false, false, false)
205            };
206
207            virtual_fs::OpenOptionsConfig {
208                read: adjusted_rights.contains(Rights::FD_READ),
209                write: write_permission,
210                create_new: create_permission && o_flags.contains(Oflags::EXCL),
211                create: create_permission,
212                append: append_permission,
213                truncate: truncate_permission,
214            }
215        }
216        Err(_) => virtual_fs::OpenOptionsConfig {
217            append: fs_flags.contains(Fdflags::APPEND),
218            write: adjusted_rights.contains(Rights::FD_WRITE),
219            read: adjusted_rights.contains(Rights::FD_READ),
220            create_new: o_flags.contains(Oflags::CREATE) && o_flags.contains(Oflags::EXCL),
221            create: o_flags.contains(Oflags::CREATE),
222            truncate: o_flags.contains(Oflags::TRUNC),
223        },
224    };
225
226    let parent_rights = virtual_fs::OpenOptionsConfig {
227        read: working_dir.inner.rights.contains(Rights::FD_READ),
228        write: working_dir.inner.rights.contains(Rights::FD_WRITE),
229        // The parent is a directory, which is why these options
230        // aren't inherited from the parent (append / truncate doesn't work on directories)
231        create_new: true,
232        create: true,
233        append: true,
234        truncate: true,
235    };
236
237    let minimum_rights = target_rights.minimum_rights(&parent_rights);
238
239    open_options.options(minimum_rights.clone());
240
241    // Regular files share a single inode-level handle across all WASIX file
242    // descriptors, so prefer opening that shared handle with duplex access.
243    // That lets a later read-only fd keep working after an earlier write-only
244    // open (and vice versa). If the backing filesystem denies duplex access,
245    // fall back to the narrower requested mode.
246    let open_shared_file_handle =
247        |path: &std::path::Path,
248         requested_config: virtual_fs::OpenOptionsConfig,
249         shared_config: virtual_fs::OpenOptionsConfig|
250         -> Result<Box<dyn VirtualFile + Send + Sync + 'static>, Errno> {
251            let mut open_options = state.fs_new_open_options();
252            open_options.options(shared_config.clone());
253            match open_options.open(path) {
254                Ok(handle) => Ok(handle),
255                Err(FsError::PermissionDenied)
256                    if shared_config.read != requested_config.read
257                        || shared_config.write != requested_config.write =>
258                {
259                    let mut open_options = state.fs_new_open_options();
260                    open_options.options(requested_config);
261                    open_options.open(path).map_err(fs_error_into_wasi_err)
262                }
263                Err(err) => Err(fs_error_into_wasi_err(err)),
264            }
265        };
266
267    let orig_path = path;
268
269    if let Ok(inode) = maybe_inode {
270        // Phase A: path resolution only — symlink follow recurses without committing.
271        {
272            let guard = inode.read();
273            if let Kind::Symlink {
274                base_po_dir,
275                path_to_symlink,
276                relative_path,
277            } = guard.deref()
278            {
279                let (resolved_base_fd, resolved_path) = if relative_path.is_absolute() {
280                    (VIRTUAL_ROOT_FD, relative_path.clone())
281                } else {
282                    let mut resolved_path = path_to_symlink.clone();
283                    resolved_path.pop();
284                    resolved_path.push(relative_path);
285                    (*base_po_dir, resolved_path)
286                };
287                drop(guard);
288                return path_open_internal(
289                    env,
290                    resolved_base_fd,
291                    __WASI_LOOKUP_SYMLINK_FOLLOW,
292                    &resolved_path.to_string_lossy(),
293                    o_flags,
294                    fs_rights_base,
295                    fs_rights_inheriting,
296                    fs_flags,
297                    fd_flags,
298                    with_fd,
299                );
300            }
301        }
302
303        if o_flags.contains(Oflags::EXCL) && o_flags.contains(Oflags::CREATE) {
304            return Ok(Err(Errno::Exist));
305        }
306
307        // Open-mode inputs derived from syscall args only (no inode-state decisions).
308        let file_requested_config = open_options
309            .write(minimum_rights.write)
310            .create(minimum_rights.create)
311            .append(false)
312            .truncate(minimum_rights.truncate)
313            .get_config();
314        let file_shared_config = virtual_fs::OpenOptionsConfig {
315            read: true,
316            write: true,
317            ..file_requested_config.clone()
318        };
319        let requires_stronger_handle =
320            minimum_rights.write || minimum_rights.truncate || minimum_rights.create;
321        let mut file_open_flags = open_flags;
322        if minimum_rights.read {
323            file_open_flags |= Fd::READ;
324        }
325        if minimum_rights.write {
326            file_open_flags |= Fd::WRITE;
327        }
328        if minimum_rights.create {
329            file_open_flags |= Fd::CREATE;
330        }
331        if minimum_rights.truncate {
332            file_open_flags |= Fd::TRUNCATE;
333        }
334
335        // Phase B: fd_map first; every inode-dependent decision under lock. For regular
336        // files keep inode write through insert_fd so handle install and acquire_handle()
337        // cannot interleave with close on this inode.
338        let mut fd_map = state.fs.fd_map.write().unwrap();
339        let mut guard = inode.write();
340        let out_fd = match guard.deref_mut() {
341            Kind::File {
342                handle,
343                path,
344                fd: Some(special_fd),
345                ..
346            } => {
347                assert!(handle.is_some());
348                *special_fd
349            }
350            Kind::File {
351                handle,
352                path,
353                fd: None,
354                ..
355            } => {
356                if o_flags.contains(Oflags::DIRECTORY) || orig_path.ends_with('/') {
357                    return Ok(Err(Errno::Notdir));
358                }
359
360                // Install or refresh the shared inode handle before checking for special
361                // stdio paths (/dev/stdin, /dev/stdout, /dev/stderr). DeviceFile stubs
362                // only report get_special_fd() once the backing open has run.
363                if handle.is_none() || requires_stronger_handle {
364                    let file = wasi_try_ok_ok!(open_shared_file_handle(
365                        path.as_path(),
366                        file_requested_config.clone(),
367                        file_shared_config.clone(),
368                    ));
369                    if handle.is_none() {
370                        *handle = Some(Arc::new(std::sync::RwLock::new(file)));
371                    } else {
372                        let mut existing = handle.as_ref().unwrap().write().unwrap();
373                        *existing = file;
374                    }
375                }
376
377                if let Some(file_handle) = handle.as_ref()
378                    && let Some(special_fd) = {
379                        let file = file_handle.read().unwrap();
380                        file.get_special_fd()
381                    }
382                {
383                    drop(guard);
384                    let dup_fd = wasi_try_ok_ok!(WasiFs::clone_fd_locked(
385                        &state.fs,
386                        &mut fd_map,
387                        special_fd,
388                        0,
389                        None,
390                    ));
391                    trace!(%dup_fd);
392                    return Ok(Ok(dup_fd));
393                }
394
395                let out_fd = wasi_try_ok_ok!(insert_fd_locked(
396                    &mut fd_map,
397                    state,
398                    adjusted_rights,
399                    adjusted_rights_inheriting,
400                    fs_flags,
401                    fd_flags,
402                    file_open_flags,
403                    inode.clone(),
404                    with_fd,
405                ));
406                drop(guard);
407                out_fd
408            }
409            Kind::Buffer { .. } => unimplemented!("wasi::path_open for Buffer type files"),
410            Kind::Root { .. } => {
411                if !o_flags.contains(Oflags::DIRECTORY) {
412                    return Ok(Err(Errno::Isdir));
413                }
414                drop(guard);
415                wasi_try_ok_ok!(insert_fd_locked(
416                    &mut fd_map,
417                    state,
418                    adjusted_rights,
419                    adjusted_rights_inheriting,
420                    fs_flags,
421                    fd_flags,
422                    open_flags,
423                    inode,
424                    with_fd,
425                ))
426            }
427            Kind::Dir { .. } => {
428                if fs_rights_base.contains(Rights::FD_WRITE) {
429                    return Ok(Err(Errno::Isdir));
430                }
431                drop(guard);
432                wasi_try_ok_ok!(insert_fd_locked(
433                    &mut fd_map,
434                    state,
435                    adjusted_rights,
436                    adjusted_rights_inheriting,
437                    fs_flags,
438                    fd_flags,
439                    open_flags,
440                    inode,
441                    with_fd,
442                ))
443            }
444            Kind::Socket { .. }
445            | Kind::PipeTx { .. }
446            | Kind::PipeRx { .. }
447            | Kind::DuplexPipe { .. }
448            | Kind::EventNotifications { .. }
449            | Kind::Epoll { .. } => {
450                drop(guard);
451                wasi_try_ok_ok!(insert_fd_locked(
452                    &mut fd_map,
453                    state,
454                    adjusted_rights,
455                    adjusted_rights_inheriting,
456                    fs_flags,
457                    fd_flags,
458                    open_flags,
459                    inode,
460                    with_fd,
461                ))
462            }
463            Kind::Symlink { .. } => unreachable!("symlinks are resolved in phase A"),
464        };
465        Ok(Ok(out_fd))
466    } else {
467        // less-happy path, we have to try to create the file
468        if o_flags.contains(Oflags::CREATE) {
469            if o_flags.contains(Oflags::DIRECTORY) {
470                return Ok(Err(Errno::Notdir));
471            }
472
473            // Trailing slash matters. But the underlying opener normalizes it away later.
474            if path.ends_with('/') {
475                return Ok(Err(Errno::Isdir));
476            }
477
478            // strip end file name
479
480            let (parent_inode, new_entity_name) =
481                wasi_try_ok_ok!(state.fs.get_parent_inode_at_path(
482                    inodes,
483                    effective_dirfd,
484                    &path_arg,
485                    follow_symlinks
486                ));
487            let new_file_host_path = {
488                let guard = parent_inode.read();
489                match guard.deref() {
490                    Kind::Dir { path, .. } => {
491                        let mut new_path = path.clone();
492                        new_path.push(&new_entity_name);
493                        new_path
494                    }
495                    Kind::Root { .. } => {
496                        let mut new_path = std::path::PathBuf::new();
497                        new_path.push(&new_entity_name);
498                        new_path
499                    }
500                    _ => return Ok(Err(Errno::Notdir)),
501                }
502            };
503            // Host create, inode wiring, and fd insert run under one fd_map write lock so
504            // no other thread can observe the inode (or clear its handle) without a map entry.
505            let mut fd_map = state.fs.fd_map.write().unwrap();
506
507            let requested_config = open_options
508                .read(minimum_rights.read)
509                .append(minimum_rights.append)
510                .write(minimum_rights.write)
511                .create_new(true)
512                .get_config();
513            let shared_config = virtual_fs::OpenOptionsConfig {
514                read: true,
515                write: true,
516                ..requested_config.clone()
517            };
518
519            if minimum_rights.read {
520                open_flags |= Fd::READ;
521            }
522            if minimum_rights.write {
523                open_flags |= Fd::WRITE;
524            }
525            if minimum_rights.create_new {
526                open_flags |= Fd::CREATE;
527            }
528            if minimum_rights.truncate {
529                open_flags |= Fd::TRUNCATE;
530            }
531
532            let handle = match open_shared_file_handle(
533                new_file_host_path.as_path(),
534                requested_config,
535                shared_config,
536            ) {
537                Ok(handle) => Some(handle),
538                Err(err) => {
539                    if err == Errno::Exist {
540                        return Ok(Err(Errno::Perm));
541                    }
542                    return Ok(Err(err));
543                }
544            };
545
546            let new_inode = {
547                let kind = Kind::File {
548                    handle: handle.map(|a| Arc::new(std::sync::RwLock::new(a))),
549                    path: new_file_host_path,
550                    fd: None,
551                };
552                wasi_try_ok_ok!(
553                    state
554                        .fs
555                        .create_inode(inodes, kind, false, new_entity_name.clone())
556                )
557            };
558
559            {
560                let mut guard = parent_inode.write();
561                if let Kind::Dir { entries, .. } = guard.deref_mut() {
562                    entries.insert(new_entity_name, new_inode.clone());
563                }
564            }
565
566            Ok(Ok(wasi_try_ok_ok!(insert_fd_locked(
567                &mut fd_map,
568                state,
569                adjusted_rights,
570                adjusted_rights_inheriting,
571                fs_flags,
572                fd_flags,
573                open_flags,
574                new_inode,
575                with_fd,
576            ))))
577        } else {
578            Ok(Err(maybe_inode.unwrap_err()))
579        }
580    }
581}
582
583fn insert_fd_locked(
584    fd_map: &mut FdList,
585    _state: &WasiState,
586    adjusted_rights: Rights,
587    adjusted_rights_inheriting: Rights,
588    fs_flags: Fdflags,
589    fd_flags: Fdflagsext,
590    open_flags: u16,
591    inode: InodeGuard,
592    with_fd: Option<WasiFd>,
593) -> Result<WasiFd, Errno> {
594    // TODO: check and reduce these
595    // TODO: ensure a mutable fd to root can never be opened
596    WasiFs::insert_fd_locked(
597        fd_map,
598        adjusted_rights,
599        adjusted_rights_inheriting,
600        fs_flags,
601        fd_flags,
602        open_flags,
603        inode,
604        with_fd,
605        with_fd.is_some(),
606    )
607}