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    let state = env.state.deref();
129    let inodes = &state.inodes;
130    let follow_symlinks = dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0;
131    let effective_dirfd = if path.starts_with('/') {
132        VIRTUAL_ROOT_FD
133    } else {
134        dirfd
135    };
136    let path_arg = std::path::PathBuf::from(path);
137    let working_dir = match state.fs.get_fd(effective_dirfd) {
138        Ok(fd) => fd,
139        Err(err) => return Ok(Err(err)),
140    };
141    let maybe_inode = state
142        .fs
143        .get_inode_at_path(inodes, effective_dirfd, path, follow_symlinks);
144    let working_dir_rights_inheriting = working_dir.inner.rights_inheriting;
145
146    // ASSUMPTION: open rights apply recursively
147    if !working_dir.inner.rights.contains(Rights::PATH_OPEN) {
148        return Ok(Err(Errno::Access));
149    }
150
151    let mut open_flags = 0;
152    // TODO: traverse rights of dirs properly
153    // COMMENTED OUT: WASI isn't giving appropriate rights here when opening
154    //              TODO: look into this; file a bug report if this is a bug
155    //
156    // Maximum rights: should be the working dir rights
157    // Minimum rights: whatever rights are provided
158    let adjusted_rights = /*fs_rights_base &*/ working_dir_rights_inheriting;
159    let mut open_options = state.fs_new_open_options();
160
161    let target_rights = match maybe_inode {
162        Ok(_) => {
163            let write_permission = adjusted_rights.contains(Rights::FD_WRITE);
164
165            // append, truncate, and create all require the permission to write
166            let (append_permission, truncate_permission, create_permission) = if write_permission {
167                (
168                    fs_flags.contains(Fdflags::APPEND),
169                    o_flags.contains(Oflags::TRUNC),
170                    o_flags.contains(Oflags::CREATE),
171                )
172            } else {
173                (false, false, false)
174            };
175
176            virtual_fs::OpenOptionsConfig {
177                read: fs_rights_base.contains(Rights::FD_READ),
178                write: write_permission,
179                create_new: create_permission && o_flags.contains(Oflags::EXCL),
180                create: create_permission,
181                append: append_permission,
182                truncate: truncate_permission,
183            }
184        }
185        Err(_) => virtual_fs::OpenOptionsConfig {
186            append: fs_flags.contains(Fdflags::APPEND),
187            write: fs_rights_base.contains(Rights::FD_WRITE),
188            read: fs_rights_base.contains(Rights::FD_READ),
189            create_new: o_flags.contains(Oflags::CREATE) && o_flags.contains(Oflags::EXCL),
190            create: o_flags.contains(Oflags::CREATE),
191            truncate: o_flags.contains(Oflags::TRUNC),
192        },
193    };
194
195    let parent_rights = virtual_fs::OpenOptionsConfig {
196        read: working_dir.inner.rights.contains(Rights::FD_READ),
197        write: working_dir.inner.rights.contains(Rights::FD_WRITE),
198        // The parent is a directory, which is why these options
199        // aren't inherited from the parent (append / truncate doesn't work on directories)
200        create_new: true,
201        create: true,
202        append: true,
203        truncate: true,
204    };
205
206    let minimum_rights = target_rights.minimum_rights(&parent_rights);
207
208    open_options.options(minimum_rights.clone());
209
210    let orig_path = path;
211
212    let inode = if let Ok(inode) = maybe_inode {
213        // Happy path, we found the file we're trying to open
214        let processing_inode = inode.clone();
215        let mut guard = processing_inode.write();
216
217        let deref_mut = guard.deref_mut();
218
219        if o_flags.contains(Oflags::EXCL) && o_flags.contains(Oflags::CREATE) {
220            return Ok(Err(Errno::Exist));
221        }
222
223        match deref_mut {
224            Kind::File {
225                handle, path, fd, ..
226            } => {
227                if let Some(special_fd) = fd {
228                    // short circuit if we're dealing with a special file
229                    assert!(handle.is_some());
230                    return Ok(Ok(*special_fd));
231                }
232                if o_flags.contains(Oflags::DIRECTORY) || orig_path.ends_with('/') {
233                    return Ok(Err(Errno::Notdir));
234                }
235
236                let open_options = open_options
237                    .write(minimum_rights.write)
238                    .create(minimum_rights.create)
239                    .append(false)
240                    .truncate(minimum_rights.truncate);
241
242                if minimum_rights.read {
243                    open_flags |= Fd::READ;
244                }
245                if minimum_rights.write {
246                    open_flags |= Fd::WRITE;
247                }
248                if minimum_rights.create {
249                    open_flags |= Fd::CREATE;
250                }
251                if minimum_rights.truncate {
252                    open_flags |= Fd::TRUNCATE;
253                }
254                // Keep a stable shared handle per inode whenever possible, but reopen it
255                // when this open requires stronger rights than the existing handle may have.
256                let requires_stronger_handle =
257                    minimum_rights.write || minimum_rights.truncate || minimum_rights.create;
258                if handle.is_none() {
259                    *handle = Some(Arc::new(std::sync::RwLock::new(wasi_try_ok_ok!(
260                        open_options.open(&path).map_err(fs_error_into_wasi_err)
261                    ))));
262                } else if requires_stronger_handle {
263                    let mut file = handle.as_ref().unwrap().write().unwrap();
264                    *file =
265                        wasi_try_ok_ok!(open_options.open(&path).map_err(fs_error_into_wasi_err));
266                }
267
268                if let Some(handle) = handle {
269                    let handle = handle.read().unwrap();
270                    if let Some(fd) = handle.get_special_fd() {
271                        // We clone the file descriptor so that when its closed
272                        // nothing bad happens
273                        let dup_fd = wasi_try_ok_ok!(state.fs.clone_fd(fd));
274                        trace!(
275                            %dup_fd
276                        );
277
278                        // some special files will return a constant FD rather than
279                        // actually open the file (/dev/stdin, /dev/stdout, /dev/stderr)
280                        return Ok(Ok(dup_fd));
281                    }
282                }
283            }
284            Kind::Buffer { .. } => unimplemented!("wasi::path_open for Buffer type files"),
285            Kind::Root { .. } => {
286                if !o_flags.contains(Oflags::DIRECTORY) {
287                    return Ok(Err(Errno::Isdir));
288                }
289            }
290            Kind::Dir { .. } => {
291                if fs_rights_base.contains(Rights::FD_WRITE) {
292                    return Ok(Err(Errno::Isdir));
293                }
294            }
295            Kind::Socket { .. }
296            | Kind::PipeTx { .. }
297            | Kind::PipeRx { .. }
298            | Kind::DuplexPipe { .. }
299            | Kind::EventNotifications { .. }
300            | Kind::Epoll { .. } => {}
301            Kind::Symlink {
302                base_po_dir,
303                path_to_symlink,
304                relative_path,
305            } => {
306                // Resolve the symlink via the existing path traversal logic and restart
307                // path_open with lookup-follow semantics for this resolved path.
308                let (resolved_base_fd, resolved_path) = if relative_path.is_absolute() {
309                    (VIRTUAL_ROOT_FD, relative_path.clone())
310                } else {
311                    let mut resolved_path = path_to_symlink.clone();
312                    resolved_path.pop();
313                    resolved_path.push(relative_path);
314                    (*base_po_dir, resolved_path)
315                };
316                return path_open_internal(
317                    env,
318                    resolved_base_fd,
319                    __WASI_LOOKUP_SYMLINK_FOLLOW,
320                    &resolved_path.to_string_lossy(),
321                    o_flags,
322                    fs_rights_base,
323                    fs_rights_inheriting,
324                    fs_flags,
325                    fd_flags,
326                    with_fd,
327                );
328            }
329        }
330        inode
331    } else {
332        // less-happy path, we have to try to create the file
333        if o_flags.contains(Oflags::CREATE) {
334            if o_flags.contains(Oflags::DIRECTORY) {
335                return Ok(Err(Errno::Notdir));
336            }
337
338            // Trailing slash matters. But the underlying opener normalizes it away later.
339            if path.ends_with('/') {
340                return Ok(Err(Errno::Isdir));
341            }
342
343            // strip end file name
344
345            let (parent_inode, new_entity_name) =
346                wasi_try_ok_ok!(state.fs.get_parent_inode_at_path(
347                    inodes,
348                    effective_dirfd,
349                    &path_arg,
350                    follow_symlinks
351                ));
352            let new_file_host_path = {
353                let guard = parent_inode.read();
354                match guard.deref() {
355                    Kind::Dir { path, .. } => {
356                        let mut new_path = path.clone();
357                        new_path.push(&new_entity_name);
358                        new_path
359                    }
360                    Kind::Root { .. } => {
361                        let mut new_path = std::path::PathBuf::new();
362                        new_path.push(&new_entity_name);
363                        new_path
364                    }
365                    _ => return Ok(Err(Errno::Inval)),
366                }
367            };
368            // once we got the data we need from the parent, we lookup the host file
369            // todo: extra check that opening with write access is okay
370            let handle = {
371                // We set create_new because the path already didn't resolve to an existing file,
372                // so it must be created.
373                let open_options = open_options
374                    .read(minimum_rights.read)
375                    .append(minimum_rights.append)
376                    .write(minimum_rights.write)
377                    .create_new(true);
378
379                if minimum_rights.read {
380                    open_flags |= Fd::READ;
381                }
382                if minimum_rights.write {
383                    open_flags |= Fd::WRITE;
384                }
385                if minimum_rights.create_new {
386                    open_flags |= Fd::CREATE;
387                }
388                if minimum_rights.truncate {
389                    open_flags |= Fd::TRUNCATE;
390                }
391
392                match open_options.open(&new_file_host_path) {
393                    Ok(handle) => Some(handle),
394                    Err(err) => {
395                        // Even though the file does not exist, it still failed to create with
396                        // `AlreadyExists` error.  This can happen if the path resolves to a
397                        // symlink that points outside the FS sandbox.
398                        if err == FsError::AlreadyExists {
399                            return Ok(Err(Errno::Perm));
400                        }
401
402                        return Ok(Err(fs_error_into_wasi_err(err)));
403                    }
404                }
405            };
406
407            let new_inode = {
408                let kind = Kind::File {
409                    handle: handle.map(|a| Arc::new(std::sync::RwLock::new(a))),
410                    path: new_file_host_path,
411                    fd: None,
412                };
413                wasi_try_ok_ok!(
414                    state
415                        .fs
416                        .create_inode(inodes, kind, false, new_entity_name.clone())
417                )
418            };
419
420            {
421                let mut guard = parent_inode.write();
422                if let Kind::Dir { entries, .. } = guard.deref_mut() {
423                    entries.insert(new_entity_name, new_inode.clone());
424                }
425            }
426
427            new_inode
428        } else {
429            return Ok(Err(maybe_inode.unwrap_err()));
430        }
431    };
432
433    // TODO: check and reduce these
434    // TODO: ensure a mutable fd to root can never be opened
435    let out_fd = wasi_try_ok_ok!(if let Some(fd) = with_fd {
436        state
437            .fs
438            .with_fd(
439                adjusted_rights,
440                fs_rights_inheriting,
441                fs_flags,
442                fd_flags,
443                open_flags,
444                inode,
445                fd,
446            )
447            .map(|_| fd)
448    } else {
449        state.fs.create_fd(
450            adjusted_rights,
451            fs_rights_inheriting,
452            fs_flags,
453            fd_flags,
454            open_flags,
455            inode,
456        )
457    });
458
459    Ok(Ok(out_fd))
460}