wasmer_wasix/syscalls/wasix/
path_open2.rs

1use super::*;
2use crate::syscalls::*;
3
4/// ### `path_open()`
5/// Open file located at the given path
6/// Inputs:
7/// - `Fd dirfd`
8///     The fd corresponding to the directory that the file is in
9/// - `LookupFlags dirflags`
10///     Flags specifying how the path will be resolved
11/// - `char *path`
12///     The path of the file or directory to open
13/// - `u32 path_len`
14///     The length of the `path` string
15/// - `Oflags o_flags`
16///     How the file will be opened
17/// - `Rights fs_rights_base`
18///     The rights of the created file descriptor
19/// - `Rights fs_rightsinheriting`
20///     The rights of file descriptors derived from the created file descriptor
21/// - `Fdflags fs_flags`
22///     The flags of the file descriptor
23/// Output:
24/// - `Fd* fd`
25///     The new file descriptor
26/// Possible Errors:
27/// - `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`
28#[instrument(level = "trace", skip_all, fields(%dirfd, path = field::Empty, follow_symlinks = field::Empty, ret_fd = field::Empty), ret)]
29pub fn path_open2<M: MemorySize>(
30    mut ctx: FunctionEnvMut<'_, WasiEnv>,
31    dirfd: WasiFd,
32    dirflags: LookupFlags,
33    path: WasmPtr<u8, M>,
34    path_len: M::Offset,
35    o_flags: Oflags,
36    fs_rights_base: Rights,
37    fs_rights_inheriting: Rights,
38    fs_flags: Fdflags,
39    fd_flags: Fdflagsext,
40    fd: WasmPtr<WasiFd, M>,
41) -> Result<Errno, WasiError> {
42    WasiEnv::do_pending_operations(&mut ctx)?;
43
44    if dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0 {
45        Span::current().record("follow_symlinks", true);
46    }
47    let env = ctx.data();
48    let (memory, mut state, mut inodes) =
49        unsafe { env.get_memory_and_wasi_state_and_inodes(&ctx, 0) };
50    /* TODO: find actual upper bound on name size (also this is a path, not a name :think-fish:) */
51    let path_len64: u64 = path_len.into();
52    if path_len64 > 1024u64 * 1024u64 {
53        return Ok(Errno::Nametoolong);
54    }
55
56    if path_len64 == 0 {
57        return Ok(Errno::Noent);
58    }
59
60    // o_flags:
61    // - __WASI_O_CREAT (create if it does not exist)
62    // - __WASI_O_DIRECTORY (fail if not dir)
63    // - __WASI_O_EXCL (fail if file exists)
64    // - __WASI_O_TRUNC (truncate size to 0)
65
66    let path_string = unsafe { get_input_str_ok!(&memory, path, path_len) };
67    Span::current().record("path", path_string.as_str());
68
69    let out_fd = wasi_try_ok!(path_open_internal(
70        ctx.data(),
71        dirfd,
72        dirflags,
73        &path_string,
74        o_flags,
75        fs_rights_base,
76        fs_rights_inheriting,
77        fs_flags,
78        fd_flags,
79        None,
80    )?);
81    let env = ctx.data();
82
83    #[cfg(feature = "journal")]
84    if env.enable_journal {
85        JournalEffector::save_path_open(
86            &mut ctx,
87            out_fd,
88            dirfd,
89            dirflags,
90            path_string,
91            o_flags,
92            fs_rights_base,
93            fs_rights_inheriting,
94            fs_flags,
95            fd_flags,
96        )
97        .map_err(|err| {
98            tracing::error!("failed to save unlink event - {}", err);
99            WasiError::Exit(ExitCode::from(Errno::Fault))
100        })?;
101    }
102
103    let env = ctx.data();
104    let (memory, mut state, mut inodes) =
105        unsafe { env.get_memory_and_wasi_state_and_inodes(&ctx, 0) };
106
107    Span::current().record("ret_fd", out_fd);
108
109    let fd_ref = fd.deref(&memory);
110    wasi_try_mem_ok!(fd_ref.write(out_fd));
111
112    Ok(Errno::Success)
113}
114
115pub(crate) fn path_open_internal(
116    env: &WasiEnv,
117    dirfd: WasiFd,
118    dirflags: LookupFlags,
119    path: &str,
120    o_flags: Oflags,
121    fs_rights_base: Rights,
122    fs_rights_inheriting: Rights,
123    fs_flags: Fdflags,
124    fd_flags: Fdflagsext,
125    with_fd: Option<WasiFd>,
126) -> Result<Result<WasiFd, Errno>, WasiError> {
127    let state = env.state.deref();
128    let inodes = &state.inodes;
129
130    let path_arg = std::path::PathBuf::from(&path);
131    let maybe_inode = state.fs.get_inode_at_path(
132        inodes,
133        dirfd,
134        path,
135        dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0,
136    );
137
138    let working_dir = wasi_try_ok_ok!(state.fs.get_fd(dirfd));
139    let working_dir_rights_inheriting = working_dir.inner.rights_inheriting;
140
141    // ASSUMPTION: open rights apply recursively
142    if !working_dir.inner.rights.contains(Rights::PATH_OPEN) {
143        return Ok(Err(Errno::Access));
144    }
145
146    let mut open_flags = 0;
147    // TODO: traverse rights of dirs properly
148    // COMMENTED OUT: WASI isn't giving appropriate rights here when opening
149    //              TODO: look into this; file a bug report if this is a bug
150    //
151    // Maximum rights: should be the working dir rights
152    // Minimum rights: whatever rights are provided
153    let adjusted_rights = /*fs_rights_base &*/ working_dir_rights_inheriting;
154    let mut open_options = state.fs_new_open_options();
155
156    let target_rights = match maybe_inode {
157        Ok(_) => {
158            let write_permission = adjusted_rights.contains(Rights::FD_WRITE);
159
160            // append, truncate, and create all require the permission to write
161            let (append_permission, truncate_permission, create_permission) = if write_permission {
162                (
163                    fs_flags.contains(Fdflags::APPEND),
164                    o_flags.contains(Oflags::TRUNC),
165                    o_flags.contains(Oflags::CREATE),
166                )
167            } else {
168                (false, false, false)
169            };
170
171            virtual_fs::OpenOptionsConfig {
172                read: fs_rights_base.contains(Rights::FD_READ),
173                write: write_permission,
174                create_new: create_permission && o_flags.contains(Oflags::EXCL),
175                create: create_permission,
176                append: append_permission,
177                truncate: truncate_permission,
178            }
179        }
180        Err(_) => virtual_fs::OpenOptionsConfig {
181            append: fs_flags.contains(Fdflags::APPEND),
182            write: fs_rights_base.contains(Rights::FD_WRITE),
183            read: fs_rights_base.contains(Rights::FD_READ),
184            create_new: o_flags.contains(Oflags::CREATE) && o_flags.contains(Oflags::EXCL),
185            create: o_flags.contains(Oflags::CREATE),
186            truncate: o_flags.contains(Oflags::TRUNC),
187        },
188    };
189
190    let parent_rights = virtual_fs::OpenOptionsConfig {
191        read: working_dir.inner.rights.contains(Rights::FD_READ),
192        write: working_dir.inner.rights.contains(Rights::FD_WRITE),
193        // The parent is a directory, which is why these options
194        // aren't inherited from the parent (append / truncate doesn't work on directories)
195        create_new: true,
196        create: true,
197        append: true,
198        truncate: true,
199    };
200
201    let minimum_rights = target_rights.minimum_rights(&parent_rights);
202
203    open_options.options(minimum_rights.clone());
204
205    let orig_path = path;
206
207    let inode = if let Ok(inode) = maybe_inode {
208        // Happy path, we found the file we're trying to open
209        let processing_inode = inode.clone();
210        let mut guard = processing_inode.write();
211
212        let deref_mut = guard.deref_mut();
213
214        if o_flags.contains(Oflags::EXCL) && o_flags.contains(Oflags::CREATE) {
215            return Ok(Err(Errno::Exist));
216        }
217
218        match deref_mut {
219            Kind::File {
220                handle, path, fd, ..
221            } => {
222                if let Some(special_fd) = fd {
223                    // short circuit if we're dealing with a special file
224                    assert!(handle.is_some());
225                    return Ok(Ok(*special_fd));
226                }
227                if o_flags.contains(Oflags::DIRECTORY) || orig_path.ends_with('/') {
228                    return Ok(Err(Errno::Notdir));
229                }
230
231                let open_options = open_options
232                    .write(minimum_rights.write)
233                    .create(minimum_rights.create)
234                    .append(false)
235                    .truncate(minimum_rights.truncate);
236
237                if minimum_rights.read {
238                    open_flags |= Fd::READ;
239                }
240                if minimum_rights.write {
241                    open_flags |= Fd::WRITE;
242                }
243                if minimum_rights.create {
244                    open_flags |= Fd::CREATE;
245                }
246                if minimum_rights.truncate {
247                    open_flags |= Fd::TRUNCATE;
248                }
249                // TODO: I strongly suspect that assigning the handle unconditionally
250                // breaks opening the same file multiple times.
251                *handle = Some(Arc::new(std::sync::RwLock::new(wasi_try_ok_ok!(
252                    open_options.open(&path).map_err(fs_error_into_wasi_err)
253                ))));
254
255                if let Some(handle) = handle {
256                    let handle = handle.read().unwrap();
257                    if let Some(fd) = handle.get_special_fd() {
258                        // We clone the file descriptor so that when its closed
259                        // nothing bad happens
260                        let dup_fd = wasi_try_ok_ok!(state.fs.clone_fd(fd));
261                        trace!(
262                            %dup_fd
263                        );
264
265                        // some special files will return a constant FD rather than
266                        // actually open the file (/dev/stdin, /dev/stdout, /dev/stderr)
267                        return Ok(Ok(dup_fd));
268                    }
269                }
270            }
271            Kind::Buffer { .. } => unimplemented!("wasi::path_open for Buffer type files"),
272            Kind::Root { .. } => {
273                if !o_flags.contains(Oflags::DIRECTORY) {
274                    return Ok(Err(Errno::Notcapable));
275                }
276            }
277            Kind::Dir { .. } => {
278                if fs_rights_base.contains(Rights::FD_WRITE) {
279                    return Ok(Err(Errno::Isdir));
280                }
281            }
282            Kind::Socket { .. }
283            | Kind::PipeTx { .. }
284            | Kind::PipeRx { .. }
285            | Kind::DuplexPipe { .. }
286            | Kind::EventNotifications { .. }
287            | Kind::Epoll { .. } => {}
288            Kind::Symlink {
289                base_po_dir,
290                path_to_symlink,
291                relative_path,
292            } => {
293                // I think this should return an error (because symlinks should be resolved away by the path traversal)
294                // TODO: investigate this
295                unimplemented!("SYMLINKS IN PATH_OPEN");
296            }
297        }
298        inode
299    } else {
300        // less-happy path, we have to try to create the file
301        if o_flags.contains(Oflags::CREATE) {
302            if o_flags.contains(Oflags::DIRECTORY) {
303                return Ok(Err(Errno::Notdir));
304            }
305
306            // Trailing slash matters. But the underlying opener normalizes it away later.
307            if path.ends_with('/') {
308                return Ok(Err(Errno::Isdir));
309            }
310
311            // strip end file name
312
313            let (parent_inode, new_entity_name) =
314                wasi_try_ok_ok!(state.fs.get_parent_inode_at_path(
315                    inodes,
316                    dirfd,
317                    &path_arg,
318                    dirflags & __WASI_LOOKUP_SYMLINK_FOLLOW != 0
319                ));
320            let new_file_host_path = {
321                let guard = parent_inode.read();
322                match guard.deref() {
323                    Kind::Dir { path, .. } => {
324                        let mut new_path = path.clone();
325                        new_path.push(&new_entity_name);
326                        new_path
327                    }
328                    Kind::Root { .. } => {
329                        let mut new_path = std::path::PathBuf::new();
330                        new_path.push(&new_entity_name);
331                        new_path
332                    }
333                    _ => return Ok(Err(Errno::Inval)),
334                }
335            };
336            // once we got the data we need from the parent, we lookup the host file
337            // todo: extra check that opening with write access is okay
338            let handle = {
339                // We set create_new because the path already didn't resolve to an existing file,
340                // so it must be created.
341                let open_options = open_options
342                    .read(minimum_rights.read)
343                    .append(minimum_rights.append)
344                    .write(minimum_rights.write)
345                    .create_new(true);
346
347                if minimum_rights.read {
348                    open_flags |= Fd::READ;
349                }
350                if minimum_rights.write {
351                    open_flags |= Fd::WRITE;
352                }
353                if minimum_rights.create_new {
354                    open_flags |= Fd::CREATE;
355                }
356                if minimum_rights.truncate {
357                    open_flags |= Fd::TRUNCATE;
358                }
359
360                match open_options.open(&new_file_host_path) {
361                    Ok(handle) => Some(handle),
362                    Err(err) => {
363                        // Even though the file does not exist, it still failed to create with
364                        // `AlreadyExists` error.  This can happen if the path resolves to a
365                        // symlink that points outside the FS sandbox.
366                        if err == FsError::AlreadyExists {
367                            return Ok(Err(Errno::Perm));
368                        }
369
370                        return Ok(Err(fs_error_into_wasi_err(err)));
371                    }
372                }
373            };
374
375            let new_inode = {
376                let kind = Kind::File {
377                    handle: handle.map(|a| Arc::new(std::sync::RwLock::new(a))),
378                    path: new_file_host_path,
379                    fd: None,
380                };
381                wasi_try_ok_ok!(
382                    state
383                        .fs
384                        .create_inode(inodes, kind, false, new_entity_name.clone())
385                )
386            };
387
388            {
389                let mut guard = parent_inode.write();
390                if let Kind::Dir { entries, .. } = guard.deref_mut() {
391                    entries.insert(new_entity_name, new_inode.clone());
392                }
393            }
394
395            new_inode
396        } else {
397            return Ok(Err(maybe_inode.unwrap_err()));
398        }
399    };
400
401    // TODO: check and reduce these
402    // TODO: ensure a mutable fd to root can never be opened
403    let out_fd = wasi_try_ok_ok!(if let Some(fd) = with_fd {
404        state
405            .fs
406            .with_fd(
407                adjusted_rights,
408                fs_rights_inheriting,
409                fs_flags,
410                fd_flags,
411                open_flags,
412                inode,
413                fd,
414            )
415            .map(|_| fd)
416    } else {
417        state.fs.create_fd(
418            adjusted_rights,
419            fs_rights_inheriting,
420            fs_flags,
421            fd_flags,
422            open_flags,
423            inode,
424        )
425    });
426
427    Ok(Ok(out_fd))
428}