Files
wasmtime/crates/test-programs/wasi-tests/src/bin/fd_readdir.rs
Jamey Sharp efdfc361f8 Allow WASI to open directories without O_DIRECTORY (#6163)
* Allow WASI to open directories without O_DIRECTORY

The `O_DIRECTORY` flag is a request that open should fail if the named
path is not a directory. Opening a path which turns out to be a
directory is not supposed to fail if this flag is not specified.
However, wasi-common required callers to use it when opening
directories.

With this PR, we always open the path the same way whether or not the
`O_DIRECTORY` flag is specified. However, after opening it, we `stat` it
to check whether it turned out to be a directory, and determine which
operations the file descriptor should support accordingly. In addition,
we explicitly check whether the precondition defined by `O_DIRECTORY` is
satisfied.

Closes #4947 and closes #4967, which were earlier attempts at fixing the
same issue, but which had race conditions.

prtest:full

* Add tests from #4967/#4947

This test was authored by Roman Volosatovs <rvolosatovs@riseup.net> as
part of #4947.

* Tests: Close FDs before trying to unlink files

On Windows, when opening a path which might be a directory using
`CreateFile`, cap-primitives also removes the `FILE_SHARE_DELETE` mode.

That means that if we implement WASI's `path_open` such that it always
uses `CreateFile` on Windows, for both files and directories, then
holding an open file handle prevents deletion of that file.

So I'm changing these test programs to make sure they've closed the
handle before trying to delete the file.
2023-04-21 16:55:35 +00:00

249 lines
7.8 KiB
Rust

use std::{env, mem, process, slice, str};
use wasi_tests::open_scratch_directory;
const BUF_LEN: usize = 256;
struct DirEntry {
dirent: wasi::Dirent,
name: String,
}
// Manually reading the output from fd_readdir is tedious and repetitive,
// so encapsulate it into an iterator
struct ReadDir<'a> {
buf: &'a [u8],
}
impl<'a> ReadDir<'a> {
fn from_slice(buf: &'a [u8]) -> Self {
Self { buf }
}
}
impl<'a> Iterator for ReadDir<'a> {
type Item = DirEntry;
fn next(&mut self) -> Option<DirEntry> {
unsafe {
if self.buf.len() < mem::size_of::<wasi::Dirent>() {
return None;
}
// Read the data
let dirent_ptr = self.buf.as_ptr() as *const wasi::Dirent;
let dirent = dirent_ptr.read_unaligned();
if self.buf.len() < mem::size_of::<wasi::Dirent>() + dirent.d_namlen as usize {
return None;
}
let name_ptr = dirent_ptr.offset(1) as *const u8;
// NOTE Linux syscall returns a NUL-terminated name, but WASI doesn't
let namelen = dirent.d_namlen as usize;
let slice = slice::from_raw_parts(name_ptr, namelen);
let name = str::from_utf8(slice).expect("invalid utf8").to_owned();
// Update the internal state
let delta = mem::size_of_val(&dirent) + namelen;
self.buf = &self.buf[delta..];
DirEntry { dirent, name }.into()
}
}
}
/// Return the entries plus a bool indicating EOF.
unsafe fn exec_fd_readdir(fd: wasi::Fd, cookie: wasi::Dircookie) -> (Vec<DirEntry>, bool) {
let mut buf: [u8; BUF_LEN] = [0; BUF_LEN];
let bufused =
wasi::fd_readdir(fd, buf.as_mut_ptr(), BUF_LEN, cookie).expect("failed fd_readdir");
assert!(bufused <= BUF_LEN);
let sl = slice::from_raw_parts(buf.as_ptr(), bufused);
let dirs: Vec<_> = ReadDir::from_slice(sl).collect();
let eof = bufused < BUF_LEN;
(dirs, eof)
}
unsafe fn assert_empty_dir(dir_fd: wasi::Fd) {
let stat = wasi::fd_filestat_get(dir_fd).expect("failed filestat");
let (mut dirs, eof) = exec_fd_readdir(dir_fd, 0);
assert!(eof, "expected to read the entire directory");
dirs.sort_by_key(|d| d.name.clone());
assert_eq!(dirs.len(), 2, "expected two entries in an empty directory");
let mut dirs = dirs.into_iter();
// the first entry should be `.`
let dir = dirs.next().expect("first entry is None");
assert_eq!(dir.name, ".", "first name");
assert_eq!(dir.dirent.d_type, wasi::FILETYPE_DIRECTORY, "first type");
assert_eq!(dir.dirent.d_ino, stat.ino);
assert_eq!(dir.dirent.d_namlen, 1);
// the second entry should be `..`
let dir = dirs.next().expect("second entry is None");
assert_eq!(dir.name, "..", "second name");
assert_eq!(dir.dirent.d_type, wasi::FILETYPE_DIRECTORY, "second type");
assert!(
dirs.next().is_none(),
"the directory should be seen as empty"
);
}
unsafe fn test_fd_readdir(dir_fd: wasi::Fd) {
// Check the behavior in an empty directory
assert_empty_dir(dir_fd);
// Add a file and check the behavior
let file_fd = wasi::path_open(
dir_fd,
0,
"file",
wasi::OFLAGS_CREAT,
wasi::RIGHTS_FD_READ
| wasi::RIGHTS_FD_WRITE
| wasi::RIGHTS_FD_READDIR
| wasi::RIGHTS_FD_FILESTAT_GET,
0,
0,
)
.expect("failed to create file");
assert!(
file_fd > libc::STDERR_FILENO as wasi::Fd,
"file descriptor range check",
);
let file_stat = wasi::fd_filestat_get(file_fd).expect("failed filestat");
wasi::fd_close(file_fd).expect("closing a file");
wasi::path_create_directory(dir_fd, "nested").expect("create a directory");
let nested_fd = wasi::path_open(
dir_fd,
0,
"nested",
0,
wasi::RIGHTS_FD_READ | wasi::RIGHTS_FD_READDIR | wasi::RIGHTS_FD_FILESTAT_GET,
0,
0,
)
.expect("failed to open nested directory");
assert!(
nested_fd > file_fd,
"nested directory file descriptor range check",
);
let nested_stat = wasi::fd_filestat_get(nested_fd).expect("failed filestat");
// Execute another readdir
let (mut dirs, eof) = exec_fd_readdir(dir_fd, 0);
assert!(eof, "expected to read the entire directory");
assert_eq!(dirs.len(), 4, "expected four entries");
// Save the data about the last entry. We need to do it before sorting.
let lastfile_cookie = dirs[2].dirent.d_next;
let lastfile_name = dirs[3].name.clone();
dirs.sort_by_key(|d| d.name.clone());
let mut dirs = dirs.into_iter();
let dir = dirs.next().expect("first entry is None");
assert_eq!(dir.name, ".", "first name");
let dir = dirs.next().expect("second entry is None");
assert_eq!(dir.name, "..", "second name");
let dir = dirs.next().expect("third entry is None");
// check the file info
assert_eq!(dir.name, "file", "file name doesn't match");
assert_eq!(
dir.dirent.d_type,
wasi::FILETYPE_REGULAR_FILE,
"type for the real file"
);
assert_eq!(dir.dirent.d_ino, file_stat.ino);
let dir = dirs.next().expect("fourth entry is None");
// check the directory info
assert_eq!(dir.name, "nested", "nested directory name doesn't match");
assert_eq!(
dir.dirent.d_type,
wasi::FILETYPE_DIRECTORY,
"type for the nested directory"
);
assert_eq!(dir.dirent.d_ino, nested_stat.ino);
// check if cookie works as expected
let (dirs, eof) = exec_fd_readdir(dir_fd, lastfile_cookie);
assert!(eof, "expected to read the entire directory");
assert_eq!(dirs.len(), 1, "expected one entry");
assert_eq!(dirs[0].name, lastfile_name, "name of the only entry");
// check if nested directory shows up as empty
assert_empty_dir(nested_fd);
wasi::fd_close(nested_fd).expect("closing a nested directory");
wasi::path_unlink_file(dir_fd, "file").expect("removing a file");
wasi::path_remove_directory(dir_fd, "nested").expect("removing a nested directory");
}
unsafe fn test_fd_readdir_lots(dir_fd: wasi::Fd) {
// Add a file and check the behavior
for count in 0..1000 {
let file_fd = wasi::path_open(
dir_fd,
0,
&format!("file.{}", count),
wasi::OFLAGS_CREAT,
wasi::RIGHTS_FD_READ
| wasi::RIGHTS_FD_WRITE
| wasi::RIGHTS_FD_READDIR
| wasi::RIGHTS_FD_FILESTAT_GET,
0,
0,
)
.expect("failed to create file");
assert!(
file_fd > libc::STDERR_FILENO as wasi::Fd,
"file descriptor range check",
);
wasi::fd_close(file_fd).expect("closing a file");
}
// Count the entries to ensure that we see the correct number.
let mut total = 0;
let mut cookie = 0;
loop {
let (dirs, eof) = exec_fd_readdir(dir_fd, cookie);
total += dirs.len();
if eof {
break;
}
cookie = dirs[dirs.len() - 1].dirent.d_next;
}
assert_eq!(total, 1002, "expected 1000 entries plus . and ..");
for count in 0..1000 {
wasi::path_unlink_file(dir_fd, &format!("file.{}", count)).expect("removing a file");
}
}
fn main() {
let mut args = env::args();
let prog = args.next().unwrap();
let arg = if let Some(arg) = args.next() {
arg
} else {
eprintln!("usage: {} <scratch directory>", prog);
process::exit(1);
};
// Open scratch directory
let dir_fd = match open_scratch_directory(&arg) {
Ok(dir_fd) => dir_fd,
Err(err) => {
eprintln!("{}", err);
process::exit(1)
}
};
// Run the tests.
unsafe { test_fd_readdir(dir_fd) }
unsafe { test_fd_readdir_lots(dir_fd) }
}