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