wasmer_wasix/syscalls/wasix/
path_open2.rs

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