Skip to content

Commit cbf7cbf

Browse files
authored
Introduce strongly-typed system primitives (#1561)
* Introduce strongly-typed system primitives This commit does a lot of reshuffling and even some more. It introduces strongly-typed system primitives which are: `OsFile`, `OsDir`, `Stdio`, and `OsOther`. Those primitives are separate structs now, each implementing a subset of `Handle` methods, rather than all being an enumeration of some supertype such as `OsHandle`. To summarise the structs: * `OsFile` represents a regular file, and implements fd-ops of `Handle` trait * `OsDir` represents a directory, and primarily implements path-ops, plus `readdir` and some common fd-ops such as `fdstat`, etc. * `Stdio` represents a stdio handle, and implements a subset of fd-ops such as `fdstat` _and_ `read_` and `write_vectored` calls * `OsOther` currently represents anything else and implements a set similar to that implemented by `Stdio` This commit is effectively an experiment and an excercise into better understanding what's going on for each OS resource/type under-the-hood. It's meant to give us some intuition in order to move on with the idea of having strongly-typed handles in WASI both in the syscall impl as well as at the libc level. Some more minor changes include making `OsHandle` represent an OS-specific wrapper for a raw OS handle (Unix fd or Windows handle). Also, since `OsDir` is tricky across OSes, we also have a supertype of `OsHandle` called `OsDirHandle` which may store a `DIR*` stream pointer (mainly BSD). Last but not least, the `Filetype` and `Rights` are now computed when the resource is created, rather than every time we call `Handle::get_file_type` and `Handle::get_rights`. Finally, in order to facilitate the latter, I've converted `EntryRights` into `HandleRights` and pushed them into each `Handle` implementor. * Do not adjust rights on Stdio * Clean up testing for TTY and escaping writes * Implement AsFile for dyn Handle This cleans up a lot of repeating boilerplate code todo with dynamic dispatch. * Delegate definition of OsDir to OS-specific modules Delegates defining `OsDir` struct to OS-specific modules (BSD, Linux, Emscripten, Windows). This way, `OsDir` can safely re-use `OsHandle` for raw OS handle storage, and can store some aux data such as an initialized stream ptr in case of BSD. As a result, we can safely get rid of `OsDirHandle` which IMHO was causing unnecessary noise and overcomplicating the design. On the other hand, delegating definition of `OsDir` to OS-specific modules isn't super clean in and of itself either. Perhaps there's a better way of handling this? * Check if filetype of OS handle matches WASI filetype when creating It seems prudent to check if the passed in `File` instance is of type matching that of the requested WASI filetype. In other words, we'd like to avoid situations where `OsFile` is created from a pipe. * Make AsFile fallible Return `EBADF` in `AsFile` in case a `Handle` cannot be made into a `std::fs::File`. * Remove unnecessary as_file conversion * Remove unnecessary check for TTY for Stdio handle type * Fix incorrect stdio ctors on Unix * Split Stdio into three separate types: Stdin, Stdout, Stderr * Rename PendingEntry::File to PendingEntry::OsHandle to avoid confusion * Rename OsHandle to RawOsHandle Also, since `RawOsHandle` on *nix doesn't need interior mutability wrt the inner raw file descriptor, we can safely swap the `RawFd` for `File` instance. * Add docs explaining what OsOther is * Allow for stdio to be non-character-device (e.g., piped) * Return error on bad preopen rather than panic
1 parent 528d3c1 commit cbf7cbf

39 files changed

+1635
-1065
lines changed

crates/wasi-common/src/ctx.rs

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use crate::entry::{Entry, EntryHandle};
22
use crate::fdpool::FdPool;
33
use crate::handle::Handle;
4-
use crate::sys::oshandle::{OsHandle, OsHandleExt};
4+
use crate::sys::osdir::OsDir;
5+
use crate::sys::osother::{OsOther, OsOtherExt};
6+
use crate::sys::stdio::{Stderr, StderrExt, Stdin, StdinExt, Stdout, StdoutExt};
57
use crate::virtfs::{VirtualDir, VirtualDirEntry};
68
use crate::wasi::types;
79
use crate::wasi::{Errno, Result};
810
use std::borrow::Borrow;
911
use std::cell::RefCell;
1012
use std::collections::HashMap;
13+
use std::convert::TryFrom;
1114
use std::ffi::{self, CString, OsString};
1215
use std::fs::File;
1316
use std::path::{Path, PathBuf};
@@ -32,9 +35,9 @@ pub enum WasiCtxBuilderError {
3235
/// Provided sequence of bytes contained an unexpected NUL byte.
3336
#[error("provided sequence contained an unexpected NUL byte")]
3437
UnexpectedNul(#[from] ffi::NulError),
35-
/// Provided `File` is not a directory.
36-
#[error("preopened directory path {} is not a directory", .0.display())]
37-
NotADirectory(PathBuf),
38+
/// The root of a VirtualDirEntry tree must be a VirtualDirEntry::Directory.
39+
#[error("the root of a VirtualDirEntry tree at {} must be a VirtualDirEntry::Directory", .0.display())]
40+
VirtualDirEntryRootNotADirectory(PathBuf),
3841
/// `WasiCtx` has too many opened files.
3942
#[error("context object has too many opened files")]
4043
TooManyFilesOpen,
@@ -43,8 +46,8 @@ pub enum WasiCtxBuilderError {
4346
type WasiCtxBuilderResult<T> = std::result::Result<T, WasiCtxBuilderError>;
4447

4548
enum PendingEntry {
46-
Thunk(fn() -> io::Result<OsHandle>),
47-
File(File),
49+
Thunk(fn() -> io::Result<Box<dyn Handle>>),
50+
OsHandle(File),
4851
}
4952

5053
impl std::fmt::Debug for PendingEntry {
@@ -53,9 +56,9 @@ impl std::fmt::Debug for PendingEntry {
5356
Self::Thunk(f) => write!(
5457
fmt,
5558
"PendingEntry::Thunk({:p})",
56-
f as *const fn() -> io::Result<OsHandle>
59+
f as *const fn() -> io::Result<Box<dyn Handle>>
5760
),
58-
Self::File(f) => write!(fmt, "PendingEntry::File({:?})", f),
61+
Self::OsHandle(f) => write!(fmt, "PendingEntry::OsHandle({:?})", f),
5962
}
6063
}
6164
}
@@ -105,22 +108,37 @@ impl PendingCString {
105108
}
106109
}
107110

111+
struct PendingPreopen(Box<dyn FnOnce() -> WasiCtxBuilderResult<Box<dyn Handle>>>);
112+
113+
impl PendingPreopen {
114+
fn new<F>(f: F) -> Self
115+
where
116+
F: FnOnce() -> WasiCtxBuilderResult<Box<dyn Handle>> + 'static,
117+
{
118+
Self(Box::new(f))
119+
}
120+
121+
fn into(self) -> WasiCtxBuilderResult<Box<dyn Handle>> {
122+
self.0()
123+
}
124+
}
125+
108126
/// A builder allowing customizable construction of `WasiCtx` instances.
109127
pub struct WasiCtxBuilder {
110128
stdin: Option<PendingEntry>,
111129
stdout: Option<PendingEntry>,
112130
stderr: Option<PendingEntry>,
113-
preopens: Option<Vec<(PathBuf, Box<dyn Handle>)>>,
131+
preopens: Option<Vec<(PathBuf, PendingPreopen)>>,
114132
args: Option<Vec<PendingCString>>,
115133
env: Option<HashMap<PendingCString, PendingCString>>,
116134
}
117135

118136
impl WasiCtxBuilder {
119137
/// Builder for a new `WasiCtx`.
120138
pub fn new() -> Self {
121-
let stdin = Some(PendingEntry::Thunk(OsHandle::from_null));
122-
let stdout = Some(PendingEntry::Thunk(OsHandle::from_null));
123-
let stderr = Some(PendingEntry::Thunk(OsHandle::from_null));
139+
let stdin = Some(PendingEntry::Thunk(OsOther::from_null));
140+
let stdout = Some(PendingEntry::Thunk(OsOther::from_null));
141+
let stderr = Some(PendingEntry::Thunk(OsOther::from_null));
124142

125143
Self {
126144
stdin,
@@ -167,27 +185,27 @@ impl WasiCtxBuilder {
167185

168186
/// Inherit stdin from the host process.
169187
pub fn inherit_stdin(&mut self) -> &mut Self {
170-
self.stdin = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdin())));
188+
self.stdin = Some(PendingEntry::Thunk(Stdin::stdin));
171189
self
172190
}
173191

174192
/// Inherit stdout from the host process.
175193
pub fn inherit_stdout(&mut self) -> &mut Self {
176-
self.stdout = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdout())));
194+
self.stdout = Some(PendingEntry::Thunk(Stdout::stdout));
177195
self
178196
}
179197

180-
/// Inherit stdout from the host process.
198+
/// Inherit stderr from the host process.
181199
pub fn inherit_stderr(&mut self) -> &mut Self {
182-
self.stderr = Some(PendingEntry::Thunk(|| Ok(OsHandle::stderr())));
200+
self.stderr = Some(PendingEntry::Thunk(Stderr::stderr));
183201
self
184202
}
185203

186204
/// Inherit the stdin, stdout, and stderr streams from the host process.
187205
pub fn inherit_stdio(&mut self) -> &mut Self {
188-
self.stdin = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdin())));
189-
self.stdout = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdout())));
190-
self.stderr = Some(PendingEntry::Thunk(|| Ok(OsHandle::stderr())));
206+
self.stdin = Some(PendingEntry::Thunk(Stdin::stdin));
207+
self.stdout = Some(PendingEntry::Thunk(Stdout::stdout));
208+
self.stderr = Some(PendingEntry::Thunk(Stderr::stderr));
191209
self
192210
}
193211

@@ -231,28 +249,32 @@ impl WasiCtxBuilder {
231249

232250
/// Provide a File to use as stdin
233251
pub fn stdin(&mut self, file: File) -> &mut Self {
234-
self.stdin = Some(PendingEntry::File(file));
252+
self.stdin = Some(PendingEntry::OsHandle(file));
235253
self
236254
}
237255

238256
/// Provide a File to use as stdout
239257
pub fn stdout(&mut self, file: File) -> &mut Self {
240-
self.stdout = Some(PendingEntry::File(file));
258+
self.stdout = Some(PendingEntry::OsHandle(file));
241259
self
242260
}
243261

244262
/// Provide a File to use as stderr
245263
pub fn stderr(&mut self, file: File) -> &mut Self {
246-
self.stderr = Some(PendingEntry::File(file));
264+
self.stderr = Some(PendingEntry::OsHandle(file));
247265
self
248266
}
249267

250268
/// Add a preopened directory.
251269
pub fn preopened_dir<P: AsRef<Path>>(&mut self, dir: File, guest_path: P) -> &mut Self {
252-
self.preopens.as_mut().unwrap().push((
253-
guest_path.as_ref().to_owned(),
254-
Box::new(OsHandle::from(dir)),
255-
));
270+
let preopen = PendingPreopen::new(move || {
271+
let dir = OsDir::try_from(dir).map_err(WasiCtxBuilderError::from)?;
272+
Ok(Box::new(dir))
273+
});
274+
self.preopens
275+
.as_mut()
276+
.unwrap()
277+
.push((guest_path.as_ref().to_owned(), preopen));
256278
self
257279
}
258280

@@ -277,18 +299,22 @@ impl WasiCtxBuilder {
277299
}
278300
}
279301

280-
let dir = if let VirtualDirEntry::Directory(entries) = dir {
281-
let mut dir = VirtualDir::new(true);
282-
populate_directory(entries, &mut dir);
283-
Box::new(dir)
284-
} else {
285-
panic!("the root of a VirtualDirEntry tree must be a VirtualDirEntry::Directory");
286-
};
287-
302+
let guest_path_owned = guest_path.as_ref().to_owned();
303+
let preopen = PendingPreopen::new(move || {
304+
if let VirtualDirEntry::Directory(entries) = dir {
305+
let mut dir = VirtualDir::new(true);
306+
populate_directory(entries, &mut dir);
307+
Ok(Box::new(dir))
308+
} else {
309+
Err(WasiCtxBuilderError::VirtualDirEntryRootNotADirectory(
310+
guest_path_owned,
311+
))
312+
}
313+
});
288314
self.preopens
289315
.as_mut()
290316
.unwrap()
291-
.push((guest_path.as_ref().to_owned(), dir));
317+
.push((guest_path.as_ref().to_owned(), preopen));
292318
self
293319
}
294320

@@ -336,15 +362,16 @@ impl WasiCtxBuilder {
336362
log::debug!("WasiCtx inserting entry {:?}", pending);
337363
let fd = match pending {
338364
PendingEntry::Thunk(f) => {
339-
let handle = EntryHandle::new(f()?);
340-
let entry = Entry::from(handle)?;
365+
let handle = EntryHandle::from(f()?);
366+
let entry = Entry::new(handle);
341367
entries
342368
.insert(entry)
343369
.ok_or(WasiCtxBuilderError::TooManyFilesOpen)?
344370
}
345-
PendingEntry::File(f) => {
346-
let handle = EntryHandle::new(OsHandle::from(f));
347-
let entry = Entry::from(handle)?;
371+
PendingEntry::OsHandle(f) => {
372+
let handle = OsOther::try_from(f)?;
373+
let handle = EntryHandle::new(handle);
374+
let entry = Entry::new(handle);
348375
entries
349376
.insert(entry)
350377
.ok_or(WasiCtxBuilderError::TooManyFilesOpen)?
@@ -353,13 +380,9 @@ impl WasiCtxBuilder {
353380
log::debug!("WasiCtx inserted at {:?}", fd);
354381
}
355382
// Then add the preopen entries.
356-
for (guest_path, dir) in self.preopens.take().unwrap() {
357-
if !dir.is_directory() {
358-
return Err(WasiCtxBuilderError::NotADirectory(guest_path));
359-
}
360-
361-
let handle = EntryHandle::from(dir);
362-
let mut entry = Entry::from(handle)?;
383+
for (guest_path, preopen) in self.preopens.take().unwrap() {
384+
let handle = EntryHandle::from(preopen.into()?);
385+
let mut entry = Entry::new(handle);
363386
entry.preopen_path = Some(guest_path);
364387
let fd = entries
365388
.insert(entry)

crates/wasi-common/src/entry.rs

Lines changed: 26 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
use crate::handle::Handle;
2-
use crate::wasi::types::{Filetype, Rights};
1+
use crate::handle::{Handle, HandleRights};
2+
use crate::wasi::types::Filetype;
33
use crate::wasi::{Errno, Result};
4-
use std::cell::Cell;
54
use std::ops::Deref;
65
use std::path::PathBuf;
76
use std::rc::Rc;
8-
use std::{fmt, io};
97

108
pub(crate) struct EntryHandle(Rc<dyn Handle>);
119

@@ -33,118 +31,67 @@ impl Deref for EntryHandle {
3331
}
3432
}
3533

36-
/// An abstraction struct serving as a wrapper for a host `Descriptor` object which requires
37-
/// certain rights `rights` in order to be accessed correctly.
34+
/// An abstraction struct serving as a wrapper for a `Handle` object.
3835
///
39-
/// Here, the `descriptor` field stores the host `Descriptor` object (such as a file descriptor, or
40-
/// stdin handle), and accessing it can only be done via the provided `Entry::as_descriptor` method
36+
/// Here, the `handle` field stores an instance of `Handle` type (such as a file descriptor, or
37+
/// stdin handle), and accessing it can only be done via the provided `Entry::as_handle` method
4138
/// which require a set of base and inheriting rights to be specified, verifying whether the stored
42-
/// `Descriptor` object is valid for the rights specified.
39+
/// `Handle` object is valid for the rights specified.
4340
pub(crate) struct Entry {
44-
pub(crate) file_type: Filetype,
4541
handle: EntryHandle,
46-
pub(crate) rights: Cell<EntryRights>,
4742
pub(crate) preopen_path: Option<PathBuf>,
4843
// TODO: directories
4944
}
5045

51-
/// Represents rights of an `Entry` entity, either already held or
52-
/// required.
53-
#[derive(Debug, Copy, Clone)]
54-
pub(crate) struct EntryRights {
55-
pub(crate) base: Rights,
56-
pub(crate) inheriting: Rights,
57-
}
58-
59-
impl EntryRights {
60-
pub(crate) fn new(base: Rights, inheriting: Rights) -> Self {
61-
Self { base, inheriting }
62-
}
63-
64-
/// Create new `EntryRights` instance from `base` rights only, keeping
65-
/// `inheriting` set to none.
66-
pub(crate) fn from_base(base: Rights) -> Self {
67-
Self {
68-
base,
69-
inheriting: Rights::empty(),
70-
}
71-
}
72-
73-
/// Create new `EntryRights` instance with both `base` and `inheriting`
74-
/// rights set to none.
75-
pub(crate) fn empty() -> Self {
46+
impl Entry {
47+
pub(crate) fn new(handle: EntryHandle) -> Self {
48+
let preopen_path = None;
7649
Self {
77-
base: Rights::empty(),
78-
inheriting: Rights::empty(),
50+
handle,
51+
preopen_path,
7952
}
8053
}
8154

82-
/// Check if `other` is a subset of those rights.
83-
pub(crate) fn contains(&self, other: &Self) -> bool {
84-
self.base.contains(&other.base) && self.inheriting.contains(&other.inheriting)
55+
pub(crate) fn get_file_type(&self) -> Filetype {
56+
self.handle.get_file_type()
8557
}
86-
}
8758

88-
impl fmt::Display for EntryRights {
89-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
90-
write!(
91-
f,
92-
"EntryRights {{ base: {}, inheriting: {} }}",
93-
self.base, self.inheriting
94-
)
59+
pub(crate) fn get_rights(&self) -> HandleRights {
60+
self.handle.get_rights()
9561
}
96-
}
9762

98-
impl Entry {
99-
pub(crate) fn from(handle: EntryHandle) -> io::Result<Self> {
100-
let file_type = handle.get_file_type()?;
101-
let rights = handle.get_rights()?;
102-
Ok(Self {
103-
file_type,
104-
handle,
105-
rights: Cell::new(rights),
106-
preopen_path: None,
107-
})
63+
pub(crate) fn set_rights(&self, rights: HandleRights) {
64+
self.handle.set_rights(rights)
10865
}
10966

110-
/// Convert this `Entry` into a host `Descriptor` object provided the specified
67+
/// Convert this `Entry` into a `Handle` object provided the specified
11168
/// `rights` rights are set on this `Entry` object.
11269
///
113-
/// The `Entry` can only be converted into a valid `Descriptor` object if
70+
/// The `Entry` can only be converted into a valid `Handle` object if
11471
/// the specified set of base rights, and inheriting rights encapsulated within `rights`
115-
/// `EntryRights` structure is a subset of rights attached to this `Entry`. The check is
72+
/// `HandleRights` structure is a subset of rights attached to this `Entry`. The check is
11673
/// performed using `Entry::validate_rights` method. If the check fails, `Errno::Notcapable`
11774
/// is returned.
118-
pub(crate) fn as_handle(&self, rights: &EntryRights) -> Result<EntryHandle> {
75+
pub(crate) fn as_handle(&self, rights: &HandleRights) -> Result<EntryHandle> {
11976
self.validate_rights(rights)?;
12077
Ok(self.handle.get())
12178
}
12279

123-
/// Check if this `Entry` object satisfies the specified `EntryRights`; i.e., if
80+
/// Check if this `Entry` object satisfies the specified `HandleRights`; i.e., if
12481
/// rights attached to this `Entry` object are a superset.
12582
///
12683
/// Upon unsuccessful check, `Errno::Notcapable` is returned.
127-
pub(crate) fn validate_rights(&self, rights: &EntryRights) -> Result<()> {
128-
if self.rights.get().contains(rights) {
84+
pub(crate) fn validate_rights(&self, rights: &HandleRights) -> Result<()> {
85+
let this_rights = self.handle.get_rights();
86+
if this_rights.contains(rights) {
12987
Ok(())
13088
} else {
13189
log::trace!(
13290
" | validate_rights failed: required rights = {}; actual rights = {}",
13391
rights,
134-
self.rights.get(),
92+
this_rights,
13593
);
13694
Err(Errno::Notcapable)
13795
}
13896
}
139-
140-
/// Test whether this descriptor is considered a tty within WASI.
141-
/// Note that since WASI itself lacks an `isatty` syscall and relies
142-
/// on a conservative approximation, we use the same approximation here.
143-
pub(crate) fn isatty(&self) -> bool {
144-
self.file_type == Filetype::CharacterDevice
145-
&& self
146-
.rights
147-
.get()
148-
.contains(&EntryRights::from_base(Rights::FD_SEEK | Rights::FD_TELL))
149-
}
15097
}

0 commit comments

Comments
 (0)