Repository: bytecodealliance/wasmtime
Pull request #1223 (2020-03-04 04:17:45)

Add initial Cranelift interpreter
See https://github.com/bytecodealliance/cranelift/pull/1351.

abrown: @bnjbvr, I had to commit 1bed1db to get this back in a working state. If the barrier to merging is that only want to expose features that are complete, we could remove the `clif-util interpret ...` functionality; we have to start somewhere, though, so I recommend we merge this as-is--missing instructions and all--and add a log message to `clif-util interpret ...` like `This is an unstable feature and a work in progress implementation`.
github-actions[bot]: #### Subscribe to Label Action

This issue or pull request has been labeled: "c", "r", "a", "n", "e", "l", "i", "f", "t"



To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
github-actions[bot]: #### Subscribe to Label Action

This issue or pull request has been labeled: "c", "r", "a", "n", "e", "l", "i", "f", "t"



To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
github-actions[bot]: #### Subscribe to Label Action

This issue or pull request has been labeled: "c", "r", "a", "n", "e", "l", "i", "f", "t"



To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
github-actions[bot]: #### Subscribe to Label Action

This issue or pull request has been labeled: "cranelift"

<details> <summary>Users Subscribed to "cranelift"</summary>

* @bnjbvr

</details>

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
abrown: @bnjbvr, I think is ready for another review but it would be best if we took a look at #1517 first since this uses some of the changes that PR made to `RunCommand`.
bnjbvr: Sorry, I am swamped in reviews again, and I don't know when I'd be able to do this; forwarding to other people.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e775b2511..52c8ef1b8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -404,6 +404,7 @@ dependencies = [
  "byteorder",
  "cranelift-codegen",
  "cranelift-frontend",
+ "cranelift-interpreter",
  "cranelift-native",
  "cranelift-preopt",
  "cranelift-reader",
@@ -429,6 +430,21 @@ dependencies = [
  "target-lexicon",
 ]
 
+[[package]]
+name = "cranelift-interpreter"
+version = "0.1.0"
+dependencies = [
+ "cranelift-codegen",
+ "cranelift-entity",
+ "cranelift-frontend",
+ "cranelift-reader",
+ "hashbrown 0.7.2",
+ "log",
+ "pretty_env_logger",
+ "thiserror",
+ "walkdir",
+]
+
 [[package]]
 name = "cranelift-module"
 version = "0.63.0"
@@ -474,6 +490,7 @@ version = "0.63.0"
 dependencies = [
  "cranelift-codegen",
  "target-lexicon",
+ "thiserror",
 ]
 
 [[package]]
@@ -519,6 +536,7 @@ dependencies = [
  "cranelift-faerie",
  "cranelift-filetests",
  "cranelift-frontend",
+ "cranelift-interpreter",
  "cranelift-module",
  "cranelift-native",
  "cranelift-object",
@@ -530,10 +548,12 @@ dependencies = [
  "file-per-thread-logger",
  "filecheck",
  "indicatif",
+ "log",
  "pretty_env_logger",
  "serde",
  "target-lexicon",
  "term",
+ "thiserror",
  "walkdir",
  "wat",
 ]
diff --git a/cranelift/Cargo.toml b/cranelift/Cargo.toml
index fef8b439c..f2e4ea960 100644
--- a/cranelift/Cargo.toml
+++ b/cranelift/Cargo.toml
@@ -17,6 +17,7 @@ path = "src/clif-util.rs"
 cfg-if = "0.1"
 cranelift-codegen = { path = "codegen", version = "0.63.0" }
 cranelift-entity = { path = "entity", version = "0.63.0" }
+cranelift-interpreter = { path = "interpreter", version = "*" }
 cranelift-reader = { path = "reader", version = "0.63.0" }
 cranelift-frontend = { path = "frontend", version = "0.63.0" }
 cranelift-serde = { path = "serde", version = "0.63.0", optional = true }
@@ -31,6 +32,7 @@ cranelift-preopt = { path = "preopt", version = "0.63.0" }
 cranelift = { path = "umbrella", version = "0.63.0" }
 filecheck = "0.5.0"
 clap = "2.32.0"
+log = "0.4.8"
 serde = "1.0.8"
 term = "0.6.1"
 capstone = { version = "0.6.0", optional = true }
@@ -39,6 +41,7 @@ target-lexicon = "0.10"
 pretty_env_logger = "0.4.0"
 file-per-thread-logger = "0.1.2"
 indicatif = "0.13.0"
+thiserror = "1.0.15"
 walkdir = "2.2"
 
 [features]
diff --git a/cranelift/interpreter/Cargo.toml b/cranelift/interpreter/Cargo.toml
new file mode 100644
index 000000000..bdfbd9438
--- /dev/null
+++ b/cranelift/interpreter/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "cranelift-interpreter"
+version = "0.1.0"
+authors = ["The Cranelift Project Developers"]
+description = "Interpret Cranelift IR"
+repository = "https://github.com/bytecodealliance/wasmtime"
+documentation = "https://docs.rs/cranelift-interpreter"
+categories = ["no-std"]
+license = "Apache-2.0 WITH LLVM-exception"
+readme = "README.md"
+edition = "2018"
+
+[dependencies]
+cranelift-codegen = { path = "../codegen", version = "*", default-features = false }
+cranelift-entity = { path = "../entity", version = "*" }
+cranelift-reader = { path = "../reader", version = "*" }
+hashbrown = { version = "0.7.1", optional = true }
+log = { version = "0.4.8", default-features = false }
+thiserror = "1.0.15"
+walkdir = "2.3.1"
+pretty_env_logger = "0.4.0"
+
+[dev-dependencies]
+cranelift-frontend = { path = "../frontend", version = "*" }
+
+[badges]
+maintenance = { status = "experimental" }
diff --git a/cranelift/interpreter/LICENSE b/cranelift/interpreter/LICENSE
new file mode 100644
index 000000000..f9d81955f
--- /dev/null
+++ b/cranelift/interpreter/LICENSE
@@ -0,0 +1,220 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+
+--- LLVM Exceptions to the Apache 2.0 License ----
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into an Object form of such source code, you
+may redistribute such embedded portions in such Object form without complying
+with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
+
+In addition, if you combine or link compiled forms of this Software with
+software that is licensed under the GPLv2 ("Combined Software") and if a
+court of competent jurisdiction determines that the patent provision (Section
+3), the indemnity provision (Section 9) or other Section of the License
+conflicts with the conditions of the GPLv2, you may retroactively and
+prospectively choose to deem waived or otherwise exclude such Section(s) of
+the License, but only in their entirety and only with respect to the Combined
+Software.
+
diff --git a/cranelift/interpreter/README.md b/cranelift/interpreter/README.md
new file mode 100644
index 000000000..85389ea0b
--- /dev/null
+++ b/cranelift/interpreter/README.md
@@ -0,0 +1,2 @@
+This crate provides an interpreter for Cranelift IR. It is still a work in progress, as many
+instructions are unimplemented and various implementation gaps exist. Use at your own risk.
diff --git a/cranelift/interpreter/src/environment.rs b/cranelift/interpreter/src/environment.rs
new file mode 100644
index 000000000..8ace55b48
--- /dev/null
+++ b/cranelift/interpreter/src/environment.rs
@@ -0,0 +1,82 @@
+//! Implements the function environment (e.g. a name-to-function mapping) for interpretation.
+
+use cranelift_codegen::ir::{FuncRef, Function};
+use std::collections::HashMap;
+
+#[derive(Default)]
+pub struct Environment {
+    functions: HashMap<FuncRef, Function>,
+    function_name_to_func_ref: HashMap<String, FuncRef>,
+}
+
+impl From<Function> for Environment {
+    fn from(f: Function) -> Self {
+        let func_ref = FuncRef::from_u32(0);
+        let mut function_name_to_func_ref = HashMap::new();
+        function_name_to_func_ref.insert(f.name.to_string(), func_ref);
+        let mut functions = HashMap::new();
+        functions.insert(func_ref, f);
+        Self {
+            functions,
+            function_name_to_func_ref,
+        }
+    }
+}
+
+impl Environment {
+    /// Add a function by name.
+    pub fn add(&mut self, name: String, function: Function) {
+        let func_ref = FuncRef::with_number(self.function_name_to_func_ref.len() as u32)
+            .expect("a valid function reference");
+        self.function_name_to_func_ref.insert(name, func_ref);
+        self.functions.insert(func_ref, function);
+    }
+
+    /// Retrieve a reference to a function in the environment by its name.
+    pub fn index_of(&self, name: &str) -> Option<FuncRef> {
+        self.function_name_to_func_ref.get(name).cloned()
+    }
+
+    /// Retrieve a function by its function reference.
+    pub fn get_by_func_ref(&self, func_ref: FuncRef) -> Option<&Function> {
+        self.functions.get(&func_ref)
+    }
+
+    /// Retrieve a function by its name.
+    pub fn get_by_name(&self, name: &str) -> Option<&Function> {
+        let func_ref = self.index_of(name)?;
+        self.get_by_func_ref(func_ref)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use cranelift_codegen::ir::{ExternalName, Signature};
+    use cranelift_codegen::isa::CallConv;
+
+    #[test]
+    fn addition() {
+        let mut env = Environment::default();
+        let a = "a";
+        let f = Function::new();
+
+        env.add(a.to_string(), f);
+        assert!(env.get_by_name(a).is_some());
+    }
+
+    #[test]
+    fn nonexistence() {
+        let env = Environment::default();
+        assert!(env.get_by_name("a").is_none());
+    }
+
+    #[test]
+    fn from() {
+        let name = ExternalName::testcase("test");
+        let signature = Signature::new(CallConv::Fast);
+        let func = Function::with_name_signature(name, signature);
+        let env: Environment = func.into();
+        assert_eq!(env.index_of("%test"), FuncRef::with_number(0));
+    }
+}
diff --git a/cranelift/interpreter/src/frame.rs b/cranelift/interpreter/src/frame.rs
new file mode 100644
index 000000000..751882a32
--- /dev/null
+++ b/cranelift/interpreter/src/frame.rs
@@ -0,0 +1,128 @@
+//! Implements a call frame (activation record) for the Cranelift interpreter.
+
+use cranelift_codegen::ir::{Function, Value as ValueRef};
+use cranelift_reader::DataValue;
+use std::collections::HashMap;
+
+/// Holds the mutable elements of an interpretation. At some point I thought about using
+/// Cell/RefCell to do field-level mutability, thinking that otherwise I would have to
+/// pass around a mutable object (for inst and registers) and an immutable one (for function,
+/// could be self)--in the end I decided to do exactly that but perhaps one day that will become
+/// untenable.
+#[derive(Debug)]
+pub struct Frame<'a> {
+    /// The currently executing function.
+    pub function: &'a Function,
+    /// The current mapping of SSA value-references to their actual values.
+    registers: HashMap<ValueRef, DataValue>,
+}
+
+impl<'a> Frame<'a> {
+    /// Construct a new [Frame] for a function. This allocates a slot in the hash map for each SSA
+    /// `Value` (renamed to `ValueRef` here) which should mean that no additional allocations are
+    /// needed while interpreting the frame.
+    pub fn new(function: &'a Function) -> Self {
+        Self {
+            function,
+            registers: HashMap::with_capacity(function.dfg.num_values()),
+        }
+    }
+
+    /// Construct a new [Frame] with the given `values` assigned to their corresponding slot
+    /// (from the SSA references in `parameters`) in the [Frame].
+    pub fn with_parameters(mut self, parameters: &[ValueRef], values: &[DataValue]) -> Self {
+        assert_eq!(parameters.len(), values.len());
+        for (n, v) in parameters.iter().zip(values) {
+            self.registers.insert(*n, v.clone());
+        }
+        self
+    }
+
+    /// Retrieve the actual value associated with an SSA reference.
+    #[inline]
+    pub fn get(&self, name: &ValueRef) -> &DataValue {
+        self.registers
+            .get(name)
+            .unwrap_or_else(|| panic!("unknown value: {}", name))
+    }
+
+    /// Retrieve multiple SSA references; see `get`.
+    pub fn get_all(&self, names: &[ValueRef]) -> Vec<DataValue> {
+        names.iter().map(|r| self.get(r)).cloned().collect()
+    }
+
+    /// Assign `value` to the SSA reference `name`.
+    #[inline]
+    pub fn set(&mut self, name: ValueRef, value: DataValue) -> Option<DataValue> {
+        self.registers.insert(name, value)
+    }
+
+    /// Assign to multiple SSA references; see `set`.
+    pub fn set_all(&mut self, names: &[ValueRef], values: Vec<DataValue>) {
+        assert_eq!(names.len(), values.len());
+        for (n, v) in names.iter().zip(values) {
+            self.set(*n, v);
+        }
+    }
+
+    /// Rename all of the SSA references in `old_names` to those in `new_names`. This will remove
+    /// any old references that are not in `old_names`. TODO This performs an extra allocation that
+    /// could be removed if we copied the values in the right order (i.e. when modifying in place,
+    /// we need to avoid changing a value before it is referenced).
+    pub fn rename(&mut self, old_names: &[ValueRef], new_names: &[ValueRef]) {
+        assert_eq!(old_names.len(), new_names.len());
+        let mut registers = HashMap::with_capacity(self.registers.len());
+        for (on, nn) in old_names.iter().zip(new_names) {
+            let v = self.registers.get(on).unwrap().clone();
+            registers.insert(*nn, v);
+        }
+        self.registers = registers;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use cranelift_codegen::ir::InstBuilder;
+    use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
+    use cranelift_reader::DataValue;
+
+    /// Build an empty function with a single return.
+    fn empty_function() -> Function {
+        let mut func = Function::new();
+        let mut context = FunctionBuilderContext::new();
+        let mut builder = FunctionBuilder::new(&mut func, &mut context);
+        let block = builder.create_block();
+        builder.switch_to_block(block);
+        builder.ins().return_(&[]);
+        func
+    }
+
+    #[test]
+    fn construction() {
+        let func = empty_function();
+        // Construction should not fail.
+        Frame::new(&func);
+    }
+
+    #[test]
+    fn assignment() {
+        let func = empty_function();
+        let mut frame = Frame::new(&func);
+
+        let a = ValueRef::with_number(1).unwrap();
+        let fortytwo = DataValue::I32(42);
+        frame.set(a, fortytwo.clone());
+        assert_eq!(frame.get(&a), &fortytwo);
+    }
+
+    #[test]
+    #[should_panic]
+    fn no_existing_value() {
+        let func = empty_function();
+        let frame = Frame::new(&func);
+
+        let a = ValueRef::with_number(1).unwrap();
+        frame.get(&a);
+    }
+}
diff --git a/cranelift/interpreter/src/interpreter.rs b/cranelift/interpreter/src/interpreter.rs
new file mode 100644
index 000000000..61d313360
--- /dev/null
+++ b/cranelift/interpreter/src/interpreter.rs
@@ -0,0 +1,365 @@
+//! Cranelift IR interpreter.
+//!
+//! This module contains the logic for interpreting Cranelift instructions.
+
+use crate::environment::Environment;
+use crate::frame::Frame;
+use crate::interpreter::Trap::InvalidType;
+use cranelift_codegen::ir::condcodes::IntCC;
+use cranelift_codegen::ir::{
+    Block, FuncRef, Function, Inst, InstructionData, InstructionData::*, Opcode, Opcode::*, Type,
+    Value as ValueRef, ValueList,
+};
+use cranelift_reader::{DataValue, DataValueCastFailure};
+use log::debug;
+use std::ops::{Add, Sub};
+use thiserror::Error;
+
+/// The valid control flow states.
+pub enum ControlFlow {
+    Continue,
+    ContinueAt(Block, Vec<ValueRef>),
+    Return(Vec<DataValue>),
+}
+
+impl ControlFlow {
+    /// For convenience, we can unwrap the [ControlFlow] state assuming that it is a
+    /// [ControlFlow::Return], panicking otherwise.
+    pub fn unwrap_return(self) -> Vec<DataValue> {
+        if let ControlFlow::Return(values) = self {
+            values
+        } else {
+            panic!("expected the control flow to be in the return state")
+        }
+    }
+}
+
+/// The ways interpretation can fail.
+#[derive(Error, Debug)]
+pub enum Trap {
+    #[error("unknown trap")]
+    Unknown,
+    #[error("invalid type for {1}: expected {0}")]
+    InvalidType(String, ValueRef),
+    #[error("invalid cast")]
+    InvalidCast(#[from] DataValueCastFailure),
+    #[error("the instruction is not implemented (perhaps for the given types): {0}")]
+    Unsupported(Inst),
+    #[error("reached an unreachable statement")]
+    Unreachable,
+    #[error("invalid control flow: {0}")]
+    InvalidControlFlow(String),
+    #[error("invalid function reference: {0}")]
+    InvalidFunctionReference(FuncRef),
+    #[error("invalid function name: {0}")]
+    InvalidFunctionName(String),
+}
+
+/// The Cranelift interpreter; it contains immutable elements such as the function environment and
+/// implements the Cranelift IR semantics.
+#[derive(Default)]
+pub struct Interpreter {
+    pub env: Environment,
+}
+
+/// Helper for more concise matching.
+macro_rules! binary_op {
+    ( $op:path[$arg1:ident, $arg2:ident]; [ $( $data_value_ty:ident ),* ]; $inst:ident ) => {
+        match ($arg1, $arg2) {
+            $( (DataValue::$data_value_ty(a), DataValue::$data_value_ty(b)) => { Ok(DataValue::$data_value_ty($op(a, b))) } )*
+            _ => Err(Trap::Unsupported($inst)),
+        }
+    };
+}
+
+impl Interpreter {
+    /// Construct a new [Interpreter] using the given [Environment].
+    pub fn new(env: Environment) -> Self {
+        Self { env }
+    }
+
+    /// Call a function by name; this is a helpful proxy for [Interpreter::call_by_index].
+    pub fn call_by_name(
+        &self,
+        func_name: &str,
+        arguments: &[DataValue],
+    ) -> Result<ControlFlow, Trap> {
+        let func_ref = self
+            .env
+            .index_of(func_name)
+            .ok_or_else(|| Trap::InvalidFunctionName(func_name.to_string()))?;
+        self.call_by_index(func_ref, arguments)
+    }
+
+    /// Call a function by its index in the [Environment]; this is a proxy for [Interpreter::call].
+    pub fn call_by_index(
+        &self,
+        func_ref: FuncRef,
+        arguments: &[DataValue],
+    ) -> Result<ControlFlow, Trap> {
+        match self.env.get_by_func_ref(func_ref) {
+            None => Err(Trap::InvalidFunctionReference(func_ref)),
+            Some(func) => self.call(func, arguments),
+        }
+    }
+
+    /// Interpret a call to a [Function] given its [DataValue] arguments.
+    fn call(&self, function: &Function, arguments: &[DataValue]) -> Result<ControlFlow, Trap> {
+        debug!("Call: {}({:?})", function.name, arguments);
+        let first_block = function
+            .layout
+            .blocks()
+            .next()
+            .expect("to have a first block");
+        let parameters = function.dfg.block_params(first_block);
+        let mut frame = Frame::new(function).with_parameters(parameters, arguments);
+        self.block(&mut frame, first_block)
+    }
+
+    /// Interpret a single [Block] in a [Function].
+    fn block(&self, frame: &mut Frame, block: Block) -> Result<ControlFlow, Trap> {
+        debug!("Block: {}", block);
+        for inst in frame.function.layout.block_insts(block) {
+            match self.inst(frame, inst)? {
+                ControlFlow::Continue => continue,
+                ControlFlow::ContinueAt(block, old_names) => {
+                    let new_names = frame.function.dfg.block_params(block);
+                    frame.rename(&old_names, new_names);
+                    return self.block(frame, block);
+                }
+                ControlFlow::Return(rs) => return Ok(ControlFlow::Return(rs)),
+            }
+        }
+        Err(Trap::Unreachable)
+    }
+
+    /// Interpret a single [instruction](Inst). This contains a `match`-based dispatch to the
+    /// implementations.
+    fn inst(&self, frame: &mut Frame, inst: Inst) -> Result<ControlFlow, Trap> {
+        use ControlFlow::{Continue, ContinueAt};
+        debug!("Inst: {}", &frame.function.dfg.display_inst(inst, None));
+
+        let data = &frame.function.dfg[inst];
+        match data {
+            Binary { opcode, args } => {
+                let arg1 = frame.get(&args[0]);
+                let arg2 = frame.get(&args[1]);
+                let result = match opcode {
+                    Iadd => binary_op!(Add::add[arg1, arg2]; [I8, I16, I32, I64, F32, F64]; inst),
+                    Isub => binary_op!(Sub::sub[arg1, arg2]; [I8, I16, I32, I64, F32, F64]; inst),
+                    _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+                }?;
+                frame.set(first_result(frame.function, inst), result);
+                Ok(Continue)
+            }
+
+            BinaryImm { opcode, arg, imm } => {
+                let imm = DataValue::from_integer(*imm, type_of(*arg, frame.function))?;
+                let arg = frame.get(&arg);
+                let result = match opcode {
+                    IaddImm => binary_op!(Add::add[arg, imm]; [I8, I16, I32, I64, F32, F64]; inst),
+                    IrsubImm => binary_op!(Sub::sub[arg, imm]; [I8, I16, I32, I64, F32, F64]; inst),
+                    _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+                }?;
+                frame.set(first_result(frame.function, inst), result);
+                Ok(Continue)
+            }
+
+            Branch {
+                opcode,
+                args,
+                destination,
+            } => match opcode {
+                Brnz => {
+                    let mut args = value_refs(frame.function, args);
+                    let first = args.remove(0);
+                    match frame.get(&first) {
+                        DataValue::B(false)
+                        | DataValue::I8(0)
+                        | DataValue::I16(0)
+                        | DataValue::I32(0)
+                        | DataValue::I64(0) => Ok(Continue),
+                        DataValue::B(true)
+                        | DataValue::I8(_)
+                        | DataValue::I16(_)
+                        | DataValue::I32(_)
+                        | DataValue::I64(_) => Ok(ContinueAt(*destination, args)),
+                        _ => Err(Trap::InvalidType("boolean or integer".to_string(), args[0])),
+                    }
+                }
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+            InstructionData::Call { args, func_ref, .. } => {
+                // Find the function to call.
+                let func_name = function_name_of_func_ref(*func_ref, frame.function);
+
+                // Call function.
+                let args = frame.get_all(args.as_slice(&frame.function.dfg.value_lists));
+                let result = self.call_by_name(&func_name, &args)?;
+
+                // Save results.
+                if let ControlFlow::Return(returned_values) = result {
+                    let ssa_values = frame.function.dfg.inst_results(inst);
+                    assert_eq!(
+                        ssa_values.len(),
+                        returned_values.len(),
+                        "expected result length ({}) to match SSA values length ({}): {}",
+                        returned_values.len(),
+                        ssa_values.len(),
+                        frame.function.dfg.display_inst(inst, None)
+                    );
+                    frame.set_all(ssa_values, returned_values);
+                    Ok(Continue)
+                } else {
+                    Err(Trap::InvalidControlFlow(format!(
+                        "did not return from: {}",
+                        frame.function.dfg.display_inst(inst, None)
+                    )))
+                }
+            }
+            InstructionData::Jump {
+                opcode,
+                destination,
+                args,
+            } => match opcode {
+                Opcode::Fallthrough => {
+                    Ok(ContinueAt(*destination, value_refs(frame.function, args)))
+                }
+                Opcode::Jump => Ok(ContinueAt(*destination, value_refs(frame.function, args))),
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+            IntCompareImm {
+                opcode,
+                arg,
+                cond,
+                imm,
+            } => match opcode {
+                IcmpImm => {
+                    let arg_value = match *frame.get(arg) {
+                        DataValue::I8(i) => Ok(i as i64),
+                        DataValue::I16(i) => Ok(i as i64),
+                        DataValue::I32(i) => Ok(i as i64),
+                        DataValue::I64(i) => Ok(i),
+                        _ => Err(InvalidType("integer".to_string(), *arg)),
+                    }?;
+                    let imm_value = (*imm).into();
+                    let result = match cond {
+                        IntCC::UnsignedLessThanOrEqual => arg_value <= imm_value,
+                        IntCC::Equal => arg_value == imm_value,
+                        _ => unimplemented!(
+                            "interpreter does not support condition code yet: {}",
+                            cond
+                        ),
+                    };
+                    let res = first_result(frame.function, inst);
+                    frame.set(res, DataValue::B(result));
+                    Ok(Continue)
+                }
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+            MultiAry { opcode, args } => match opcode {
+                Return => {
+                    let rs: Vec<DataValue> = args
+                        .as_slice(&frame.function.dfg.value_lists)
+                        .iter()
+                        .map(|r| frame.get(r).clone())
+                        .collect();
+                    Ok(ControlFlow::Return(rs))
+                }
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+            NullAry { opcode } => match opcode {
+                Nop => Ok(Continue),
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+            UnaryImm { opcode, imm } => match opcode {
+                Iconst => {
+                    let res = first_result(frame.function, inst);
+                    let imm_value = DataValue::from_integer(*imm, type_of(res, frame.function))?;
+                    frame.set(res, imm_value);
+                    Ok(Continue)
+                }
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+            UnaryBool { opcode, imm } => match opcode {
+                Bconst => {
+                    let res = first_result(frame.function, inst);
+                    frame.set(res, DataValue::B(*imm));
+                    Ok(Continue)
+                }
+                _ => unimplemented!("interpreter does not support opcode yet: {}", opcode),
+            },
+
+            _ => unimplemented!("interpreter does not support instruction yet: {:?}", data),
+        }
+    }
+}
+
+/// Return the first result of an instruction.
+///
+/// This helper cushions the interpreter from changes to the [Function] API.
+#[inline]
+fn first_result(function: &Function, inst: Inst) -> ValueRef {
+    function.dfg.first_result(inst)
+}
+
+/// Return a list of IR values as a vector.
+///
+/// This helper cushions the interpreter from changes to the [Function] API.
+#[inline]
+fn value_refs(function: &Function, args: &ValueList) -> Vec<ValueRef> {
+    args.as_slice(&function.dfg.value_lists).to_vec()
+}
+
+/// Return the (external) function name of `func_ref` in a local `function`. Note that this may
+/// be truncated.
+///
+/// This helper cushions the interpreter from changes to the [Function] API.
+#[inline]
+fn function_name_of_func_ref(func_ref: FuncRef, function: &Function) -> String {
+    function
+        .dfg
+        .ext_funcs
+        .get(func_ref)
+        .expect("function to exist")
+        .name
+        .to_string()
+}
+
+/// Helper for calculating the type of an IR value. TODO move to Frame?
+#[inline]
+fn type_of(value: ValueRef, function: &Function) -> Type {
+    function.dfg.value_type(value)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use cranelift_reader::parse_functions;
+
+    // Most interpreter tests should use the more ergonomic `test interpret` filetest but this
+    // unit test serves as a sanity check that the interpreter still works without all of the
+    // filetest infrastructure.
+    #[test]
+    fn sanity() {
+        let code = "function %test() -> b1 {
+        block0:
+            v0 = iconst.i32 40
+            v1 = iadd_imm v0, 3
+            v2 = irsub_imm v1, 1
+            v3 = icmp_imm eq v2, 42
+            return v3
+        }";
+
+        let func = parse_functions(code).unwrap().into_iter().next().unwrap();
+        let mut env = Environment::default();
+        env.add(func.name.to_string(), func);
+        let interpreter = Interpreter::new(env);
+        let result = interpreter
+            .call_by_name("%test", &[])
+            .unwrap()
+            .unwrap_return();
+
+        assert_eq!(result, vec![DataValue::B(true)])
+    }
+}
diff --git a/cranelift/interpreter/src/lib.rs b/cranelift/interpreter/src/lib.rs
new file mode 100644
index 000000000..cf100607c
--- /dev/null
+++ b/cranelift/interpreter/src/lib.rs
@@ -0,0 +1,7 @@
+//! Cranelift IR interpreter.
+//!
+//! This module is a project for interpreting Cranelift IR.
+
+pub mod environment;
+pub mod frame;
+pub mod interpreter;
diff --git a/cranelift/reader/Cargo.toml b/cranelift/reader/Cargo.toml
index b6185644c..fb0f8a1fd 100644
--- a/cranelift/reader/Cargo.toml
+++ b/cranelift/reader/Cargo.toml
@@ -12,6 +12,7 @@ edition = "2018"
 [dependencies]
 cranelift-codegen = { path = "../codegen", version = "0.63.0" }
 target-lexicon = "0.10"
+thiserror = "1.0.15"
 
 [badges]
 maintenance = { status = "experimental" }
diff --git a/cranelift/reader/src/lib.rs b/cranelift/reader/src/lib.rs
index 101bcb291..14e0a84b5 100644
--- a/cranelift/reader/src/lib.rs
+++ b/cranelift/reader/src/lib.rs
@@ -29,7 +29,7 @@
 pub use crate::error::{Location, ParseError, ParseResult};
 pub use crate::isaspec::{parse_options, IsaSpec, ParseOptionError};
 pub use crate::parser::{parse_functions, parse_run_command, parse_test, ParseOptions};
-pub use crate::run_command::{Comparison, DataValue, Invocation, RunCommand};
+pub use crate::run_command::{Comparison, DataValue, DataValueCastFailure, Invocation, RunCommand};
 pub use crate::sourcemap::SourceMap;
 pub use crate::testcommand::{TestCommand, TestOption};
 pub use crate::testfile::{Comment, Details, Feature, TestFile};
diff --git a/cranelift/reader/src/run_command.rs b/cranelift/reader/src/run_command.rs
index 27bd81dec..4fe06d353 100644
--- a/cranelift/reader/src/run_command.rs
+++ b/cranelift/reader/src/run_command.rs
@@ -6,9 +6,11 @@
 //! - `; run`: this assumes the function has a signature like `() -> b*`.
 //! - `; run: %fn(42, 4.2) == false`: this syntax specifies the parameters and return values.
 
-use cranelift_codegen::ir::immediates::{Ieee32, Ieee64};
-use cranelift_codegen::ir::{self, ConstantData, Type};
+use cranelift_codegen::ir::immediates::{Ieee32, Ieee64, Imm64};
+use cranelift_codegen::ir::{self, types, ConstantData, Type};
+use std::convert::TryInto;
 use std::fmt::{self, Display, Formatter};
+use thiserror::Error;
 
 /// A run command appearing in a test file.
 ///
@@ -27,17 +29,20 @@ impl RunCommand {
     ///  - for [RunCommand::Print], print the returned values from invoking the function.
     ///  - for [RunCommand::Run], compare the returned values from the invoked function and
     ///    return an `Err` with a descriptive string if the comparison fails.
+    ///
+    /// Accepts a function used for invoking the actual execution of the command. This function,
+    /// `invoked_fn`, is passed the _function name_ and _function arguments_ of the [Invocation].
     pub fn run<F>(&self, invoke_fn: F) -> Result<(), String>
     where
-        F: FnOnce(&[DataValue]) -> Vec<DataValue>,
+        F: FnOnce(&str, &[DataValue]) -> Result<Vec<DataValue>, String>,
     {
         match self {
             RunCommand::Print(invoke) => {
-                let actual = invoke_fn(&invoke.args);
+                let actual = invoke_fn(&invoke.func, &invoke.args)?;
                 println!("{} -> {}", invoke, DisplayDataValues(&actual))
             }
             RunCommand::Run(invoke, compare, expected) => {
-                let actual = invoke_fn(&invoke.args);
+                let actual = invoke_fn(&invoke.func, &invoke.args)?;
                 let matched = match compare {
                     Comparison::Equals => *expected == actual,
                     Comparison::NotEquals => *expected != actual,
@@ -107,17 +112,28 @@ pub enum DataValue {
 }
 
 impl DataValue {
+    /// Try to cast an immediate integer ([Imm64]) to the given Cranelift [Type].
+    pub fn from_integer(imm: Imm64, ty: Type) -> Result<DataValue, DataValueCastFailure> {
+        match ty {
+            types::I8 => Ok(DataValue::I8(imm.bits() as i8)),
+            types::I16 => Ok(DataValue::I16(imm.bits() as i16)),
+            types::I32 => Ok(DataValue::I32(imm.bits() as i32)),
+            types::I64 => Ok(DataValue::I64(imm.bits())),
+            _ => Err(DataValueCastFailure::FromImm64(imm, ty)),
+        }
+    }
+
     /// Return the Cranelift IR [Type] for this [DataValue].
     pub fn ty(&self) -> Type {
         match self {
-            DataValue::B(_) => ir::types::B8,
+            DataValue::B(_) => ir::types::B8, // A default type.
             DataValue::I8(_) => ir::types::I8,
             DataValue::I16(_) => ir::types::I16,
             DataValue::I32(_) => ir::types::I32,
             DataValue::I64(_) => ir::types::I64,
             DataValue::F32(_) => ir::types::F32,
             DataValue::F64(_) => ir::types::F64,
-            DataValue::V128(_) => ir::types::I8X16,
+            DataValue::V128(_) => ir::types::I8X16, // A default type.
         }
     }
 
@@ -130,24 +146,48 @@ impl DataValue {
     }
 }
 
-/// Helper for creating [From] implementations for [DataValue]
-macro_rules! from_data {
-    ( $ty:ty, $variant:ident ) => {
-        impl From<$ty> for DataValue {
-            fn from(data: $ty) -> Self {
-                DataValue::$variant(data)
+/// Record failures to cast [DataValue].
+#[derive(Error, Debug, PartialEq)]
+#[allow(missing_docs)]
+pub enum DataValueCastFailure {
+    #[error("unable to cast data value of type {0} to type {1}")]
+    TryInto(Type, Type),
+    #[error("unable to cast Imm64({0}) to a data value of type {1}")]
+    FromImm64(Imm64, Type),
+}
+
+/// Helper for creating conversion implementations for [DataValue].
+macro_rules! build_conversion_impl {
+    ( $rust_ty:ty, $data_value_ty:ident, $cranelift_ty:ident ) => {
+        impl From<$rust_ty> for DataValue {
+            fn from(data: $rust_ty) -> Self {
+                DataValue::$data_value_ty(data)
+            }
+        }
+
+        impl TryInto<$rust_ty> for DataValue {
+            type Error = DataValueCastFailure;
+            fn try_into(self) -> Result<$rust_ty, Self::Error> {
+                if let DataValue::$data_value_ty(v) = self {
+                    Ok(v)
+                } else {
+                    Err(DataValueCastFailure::TryInto(
+                        self.ty(),
+                        types::$cranelift_ty,
+                    ))
+                }
             }
         }
     };
 }
-from_data!(bool, B);
-from_data!(i8, I8);
-from_data!(i16, I16);
-from_data!(i32, I32);
-from_data!(i64, I64);
-from_data!(f32, F32);
-from_data!(f64, F64);
-from_data!([u8; 16], V128);
+build_conversion_impl!(bool, B, B8);
+build_conversion_impl!(i8, I8, I8);
+build_conversion_impl!(i16, I16, I16);
+build_conversion_impl!(i32, I32, I32);
+build_conversion_impl!(i64, I64, I64);
+build_conversion_impl!(f32, F32, F32);
+build_conversion_impl!(f64, F64, F64);
+build_conversion_impl!([u8; 16], V128, I8X16);
 
 impl Display for DataValue {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
@@ -231,7 +271,30 @@ mod test {
             .unwrap()
             .unwrap();
 
-        assert!(command.run(|_| vec![DataValue::I32(42)]).is_ok());
-        assert!(command.run(|_| vec![DataValue::I32(43)]).is_err());
+        assert!(command.run(|_, _| Ok(vec![DataValue::I32(42)])).is_ok());
+        assert!(command.run(|_, _| Ok(vec![DataValue::I32(43)])).is_err());
+    }
+
+    #[test]
+    fn type_conversions() {
+        assert_eq!(DataValue::B(true).ty(), types::B8);
+        assert_eq!(
+            TryInto::<bool>::try_into(DataValue::B(false)).unwrap(),
+            false
+        );
+        assert_eq!(
+            TryInto::<i32>::try_into(DataValue::B(false)).unwrap_err(),
+            DataValueCastFailure::TryInto(types::B8, types::I32)
+        );
+
+        assert_eq!(DataValue::V128([0; 16]).ty(), types::I8X16);
+        assert_eq!(
+            TryInto::<[u8; 16]>::try_into(DataValue::V128([0; 16])).unwrap(),
+            [0; 16]
+        );
+        assert_eq!(
+            TryInto::<i32>::try_into(DataValue::V128([0; 16])).unwrap_err(),
+            DataValueCastFailure::TryInto(types::I8X16, types::I32)
+        );
     }
 }
diff --git a/cranelift/src/clif-util.rs b/cranelift/src/clif-util.rs
index 4066ef0fd..2a8f37664 100755
--- a/cranelift/src/clif-util.rs
+++ b/cranelift/src/clif-util.rs
@@ -24,6 +24,7 @@ mod bugpoint;
 mod cat;
 mod compile;
 mod disasm;
+mod interpret;
 mod print_cfg;
 mod run;
 mod utils;
@@ -179,6 +180,13 @@ fn main() {
                 .arg(add_input_file_arg())
                 .arg(add_debug_flag()),
         )
+        .subcommand(
+            SubCommand::with_name("interpret")
+                .about("Interpret CLIF code")
+                .arg(add_verbose_flag())
+                .arg(add_input_file_arg())
+                .arg(add_debug_flag()),
+        )
         .subcommand(
             SubCommand::with_name("cat")
                 .about("Outputs .clif file")
@@ -239,6 +247,14 @@ fn main() {
             )
             .map(|_time| ())
         }
+        ("interpret", Some(rest_cmd)) => {
+            handle_debug_flag(rest_cmd.is_present("debug"));
+            interpret::run(
+                get_vec(rest_cmd.values_of("file")),
+                rest_cmd.is_present("verbose"),
+            )
+            .map(|_time| ())
+        }
         ("pass", Some(rest_cmd)) => {
             handle_debug_flag(rest_cmd.is_present("debug"));
 
diff --git a/cranelift/src/interpret.rs b/cranelift/src/interpret.rs
new file mode 100644
index 000000000..efd0b22a6
--- /dev/null
+++ b/cranelift/src/interpret.rs
@@ -0,0 +1,163 @@
+//! CLI tool to interpret Cranelift IR files.
+
+use crate::utils::iterate_files;
+use cranelift_interpreter::environment::Environment;
+use cranelift_interpreter::interpreter::{ControlFlow, Interpreter};
+use cranelift_reader::{parse_run_command, parse_test, ParseError, ParseOptions};
+use log::debug;
+use std::path::PathBuf;
+use std::{fs, io};
+use thiserror::Error;
+
+/// Run files through the Cranelift interpreter, interpreting any functions with annotations.
+pub fn run(files: Vec<String>, flag_print: bool) -> Result<(), String> {
+    let mut total = 0;
+    let mut errors = 0;
+    for file in iterate_files(files) {
+        total += 1;
+        let runner = FileInterpreter::from_path(file).map_err(|e| e.to_string())?;
+        match runner.run() {
+            Ok(_) => {
+                if flag_print {
+                    println!("{}", runner.path());
+                }
+            }
+            Err(e) => {
+                if flag_print {
+                    println!("{}: {}", runner.path(), e.to_string());
+                }
+                errors += 1;
+            }
+        }
+    }
+
+    if flag_print {
+        match total {
+            0 => println!("0 files"),
+            1 => println!("1 file"),
+            n => println!("{} files", n),
+        }
+    }
+
+    match errors {
+        0 => Ok(()),
+        1 => Err(String::from("1 failure")),
+        n => Err(format!("{} failures", n)),
+    }
+}
+
+/// Contains CLIF code that can be executed with [FileInterpreter::run].
+pub struct FileInterpreter {
+    path: Option<PathBuf>,
+    contents: String,
+}
+
+impl FileInterpreter {
+    /// Construct a file runner from a CLIF file path.
+    pub fn from_path(path: impl Into<PathBuf>) -> Result<Self, io::Error> {
+        let path = path.into();
+        debug!("New file runner from path: {}:", path.to_string_lossy());
+        let contents = fs::read_to_string(&path)?;
+        Ok(Self {
+            path: Some(path),
+            contents,
+        })
+    }
+
+    /// Construct a file runner from a CLIF code string. Currently only used for testing.
+    #[cfg(test)]
+    pub fn from_inline_code(contents: String) -> Self {
+        debug!("New file runner from inline code: {}:", &contents[..20]);
+        Self {
+            path: None,
+            contents,
+        }
+    }
+
+    /// Return the path of the file runner or `[inline code]`.
+    pub fn path(&self) -> String {
+        match self.path {
+            None => "[inline code]".to_string(),
+            Some(ref p) => p.to_string_lossy().to_string(),
+        }
+    }
+
+    /// Run the file; this searches for annotations like `; run: %fn0(42)` or
+    /// `; test: %fn0(42) == 2` and executes them, performing any test comparisons if necessary.
+    pub fn run(&self) -> Result<(), FileInterpreterFailure> {
+        // parse file
+        let test = parse_test(&self.contents, ParseOptions::default())
+            .map_err(|e| FileInterpreterFailure::ParsingClif(self.path(), e))?;
+
+        // collect functions
+        let mut env = Environment::default();
+        let mut commands = vec![];
+        for (func, details) in test.functions.into_iter() {
+            for comment in details.comments {
+                if let Some(command) = parse_run_command(comment.text, &func.signature)
+                    .map_err(|e| FileInterpreterFailure::ParsingClif(self.path(), e))?
+                {
+                    commands.push(command);
+                }
+            }
+            // Note: func.name may truncate the function name
+            env.add(func.name.to_string(), func);
+        }
+
+        // Run assertion commands
+        let interpreter = Interpreter::new(env);
+        for command in commands {
+            command
+                .run(|func_name, args| {
+                    // Because we have stored function names with a leading %, we need to re-add it.
+                    let func_name = &format!("%{}", func_name);
+                    match interpreter.call_by_name(func_name, args) {
+                        Ok(ControlFlow::Return(results)) => Ok(results),
+                        Ok(_) => panic!("Unexpected returned control flow--this is likely a bug."),
+                        Err(t) => Err(t.to_string()),
+                    }
+                })
+                .map_err(|s| FileInterpreterFailure::FailedExecution(s))?;
+        }
+
+        Ok(())
+    }
+}
+
+/// Possible sources of failure in this file.
+#[derive(Error, Debug)]
+pub enum FileInterpreterFailure {
+    #[error("failure reading file")]
+    Io(#[from] io::Error),
+    #[error("failure parsing file {0}: {1}")]
+    ParsingClif(String, ParseError),
+    #[error("failed to run function: {0}")]
+    FailedExecution(String),
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn nop() {
+        let code = String::from(
+            "
+            function %test() -> b8 {
+            block0:
+                nop
+                v1 = bconst.b8 true
+                v2 = iconst.i8 42
+                return v1
+            }
+            ; run: %test() == true
+            ",
+        );
+        FileInterpreter::from_inline_code(code).run().unwrap()
+    }
+
+    #[test]
+    fn filetests() {
+        run(vec!["../filetests/filetests/interpreter".to_string()], true).unwrap()
+    }
+}
diff --git a/cranelift/src/run.rs b/cranelift/src/run.rs
index d23cb76c8..e54d56ed2 100644
--- a/cranelift/src/run.rs
+++ b/cranelift/src/run.rs
@@ -1,13 +1,12 @@
 //! CLI tool to compile Cranelift IR files to native code in memory and execute them.
 
-use crate::utils::read_to_string;
+use crate::utils::{iterate_files, read_to_string};
 use cranelift_codegen::isa::{CallConv, TargetIsa};
 use cranelift_filetests::SingleFunctionCompiler;
 use cranelift_native::builder as host_isa_builder;
 use cranelift_reader::{parse_run_command, parse_test, Details, IsaSpec, ParseOptions};
 use std::path::PathBuf;
 use target_lexicon::Triple;
-use walkdir::WalkDir;
 
 pub fn run(files: Vec<String>, flag_print: bool) -> Result<(), String> {
     let stdin_exist = files.iter().find(|file| *file == "-").is_some();
@@ -54,29 +53,6 @@ pub fn run(files: Vec<String>, flag_print: bool) -> Result<(), String> {
     }
 }
 
-/// Iterate over all of the files passed as arguments, recursively iterating through directories
-fn iterate_files(files: Vec<String>) -> impl Iterator<Item = PathBuf> {
-    files
-        .into_iter()
-        .flat_map(WalkDir::new)
-        .filter(|f| match f {
-            Ok(d) => {
-                // filter out hidden files (starting with .)
-                !d.file_name().to_str().map_or(false, |s| s.starts_with('.'))
-                    // filter out directories
-                    && !d.file_type().is_dir()
-            }
-            Err(e) => {
-                println!("Unable to read file: {}", e);
-                false
-            }
-        })
-        .map(|f| {
-            f.expect("This should not happen: we have already filtered out the errors")
-                .into_path()
-        })
-}
-
 /// Run all functions in a file that are succeeded by "run:" comments
 fn run_single_file(path: &PathBuf) -> Result<(), String> {
     let file_contents = read_to_string(&path).map_err(|e| e.to_string())?;
@@ -98,7 +74,7 @@ fn run_file_contents(file_contents: String) -> Result<(), String> {
                 parse_run_command(comment.text, &func.signature).map_err(|e| e.to_string())?
             {
                 let compiled_fn = compiler.compile(func.clone()).map_err(|e| e.to_string())?;
-                command.run(|args| compiled_fn.call(args))?;
+                command.run(|_, args| Ok(compiled_fn.call(args)))?;
             }
         }
     }
diff --git a/cranelift/src/utils.rs b/cranelift/src/utils.rs
index 2bae5f9aa..777194953 100644
--- a/cranelift/src/utils.rs
+++ b/cranelift/src/utils.rs
@@ -6,9 +6,10 @@ use cranelift_codegen::settings::{self, FlagsOrIsa};
 use cranelift_reader::{parse_options, Location, ParseError, ParseOptionError};
 use std::fs::File;
 use std::io::{self, Read};
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::str::FromStr;
 use target_lexicon::Triple;
+use walkdir::WalkDir;
 
 /// Read an entire file into a string.
 pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
@@ -105,3 +106,26 @@ pub fn parse_sets_and_triple(
         Ok(OwnedFlagsOrIsa::Flags(settings::Flags::new(flag_builder)))
     }
 }
+
+/// Iterate over all of the files passed as arguments, recursively iterating through directories.
+pub fn iterate_files(files: Vec<String>) -> impl Iterator<Item = PathBuf> {
+    files
+        .into_iter()
+        .flat_map(WalkDir::new)
+        .filter(|f| match f {
+            Ok(d) => {
+                // Filter out hidden files (starting with .).
+                !d.file_name().to_str().map_or(false, |s| s.starts_with('.'))
+                    // Filter out directories.
+                    && !d.file_type().is_dir()
+            }
+            Err(e) => {
+                println!("Unable to read file: {}", e);
+                false
+            }
+        })
+        .map(|f| {
+            f.expect("this should not happen: we have already filtered out the errors")
+                .into_path()
+        })
+}
diff --git a/crates/wasi-common/src/ctx.rs b/crates/wasi-common/src/ctx.rs
index de66d8181..895c96ad6 100644
--- a/crates/wasi-common/src/ctx.rs
+++ b/crates/wasi-common/src/ctx.rs
@@ -1,13 +1,16 @@
 use crate::entry::{Entry, EntryHandle};
 use crate::fdpool::FdPool;
 use crate::handle::Handle;
-use crate::sys::oshandle::{OsHandle, OsHandleExt};
+use crate::sys::osdir::OsDir;
+use crate::sys::osother::{OsOther, OsOtherExt};
+use crate::sys::stdio::{Stderr, StderrExt, Stdin, StdinExt, Stdout, StdoutExt};
 use crate::virtfs::{VirtualDir, VirtualDirEntry};
 use crate::wasi::types;
 use crate::wasi::{Errno, Result};
 use std::borrow::Borrow;
 use std::cell::RefCell;
 use std::collections::HashMap;
+use std::convert::TryFrom;
 use std::ffi::{self, CString, OsString};
 use std::fs::File;
 use std::path::{Path, PathBuf};
@@ -32,9 +35,9 @@ pub enum WasiCtxBuilderError {
     /// Provided sequence of bytes contained an unexpected NUL byte.
     #[error("provided sequence contained an unexpected NUL byte")]
     UnexpectedNul(#[from] ffi::NulError),
-    /// Provided `File` is not a directory.
-    #[error("preopened directory path {} is not a directory", .0.display())]
-    NotADirectory(PathBuf),
+    /// The root of a VirtualDirEntry tree must be a VirtualDirEntry::Directory.
+    #[error("the root of a VirtualDirEntry tree at {} must be a VirtualDirEntry::Directory", .0.display())]
+    VirtualDirEntryRootNotADirectory(PathBuf),
     /// `WasiCtx` has too many opened files.
     #[error("context object has too many opened files")]
     TooManyFilesOpen,
@@ -43,8 +46,8 @@ pub enum WasiCtxBuilderError {
 type WasiCtxBuilderResult<T> = std::result::Result<T, WasiCtxBuilderError>;
 
 enum PendingEntry {
-    Thunk(fn() -> io::Result<OsHandle>),
-    File(File),
+    Thunk(fn() -> io::Result<Box<dyn Handle>>),
+    OsHandle(File),
 }
 
 impl std::fmt::Debug for PendingEntry {
@@ -53,9 +56,9 @@ impl std::fmt::Debug for PendingEntry {
             Self::Thunk(f) => write!(
                 fmt,
                 "PendingEntry::Thunk({:p})",
-                f as *const fn() -> io::Result<OsHandle>
+                f as *const fn() -> io::Result<Box<dyn Handle>>
             ),
-            Self::File(f) => write!(fmt, "PendingEntry::File({:?})", f),
+            Self::OsHandle(f) => write!(fmt, "PendingEntry::OsHandle({:?})", f),
         }
     }
 }
@@ -105,12 +108,27 @@ impl PendingCString {
     }
 }
 
+struct PendingPreopen(Box<dyn FnOnce() -> WasiCtxBuilderResult<Box<dyn Handle>>>);
+
+impl PendingPreopen {
+    fn new<F>(f: F) -> Self
+    where
+        F: FnOnce() -> WasiCtxBuilderResult<Box<dyn Handle>> + 'static,
+    {
+        Self(Box::new(f))
+    }
+
+    fn into(self) -> WasiCtxBuilderResult<Box<dyn Handle>> {
+        self.0()
+    }
+}
+
 /// A builder allowing customizable construction of `WasiCtx` instances.
 pub struct WasiCtxBuilder {
     stdin: Option<PendingEntry>,
     stdout: Option<PendingEntry>,
     stderr: Option<PendingEntry>,
-    preopens: Option<Vec<(PathBuf, Box<dyn Handle>)>>,
+    preopens: Option<Vec<(PathBuf, PendingPreopen)>>,
     args: Option<Vec<PendingCString>>,
     env: Option<HashMap<PendingCString, PendingCString>>,
 }
@@ -118,9 +136,9 @@ pub struct WasiCtxBuilder {
 impl WasiCtxBuilder {
     /// Builder for a new `WasiCtx`.
     pub fn new() -> Self {
-        let stdin = Some(PendingEntry::Thunk(OsHandle::from_null));
-        let stdout = Some(PendingEntry::Thunk(OsHandle::from_null));
-        let stderr = Some(PendingEntry::Thunk(OsHandle::from_null));
+        let stdin = Some(PendingEntry::Thunk(OsOther::from_null));
+        let stdout = Some(PendingEntry::Thunk(OsOther::from_null));
+        let stderr = Some(PendingEntry::Thunk(OsOther::from_null));
 
         Self {
             stdin,
@@ -167,27 +185,27 @@ impl WasiCtxBuilder {
 
     /// Inherit stdin from the host process.
     pub fn inherit_stdin(&mut self) -> &mut Self {
-        self.stdin = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdin())));
+        self.stdin = Some(PendingEntry::Thunk(Stdin::stdin));
         self
     }
 
     /// Inherit stdout from the host process.
     pub fn inherit_stdout(&mut self) -> &mut Self {
-        self.stdout = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdout())));
+        self.stdout = Some(PendingEntry::Thunk(Stdout::stdout));
         self
     }
 
-    /// Inherit stdout from the host process.
+    /// Inherit stderr from the host process.
     pub fn inherit_stderr(&mut self) -> &mut Self {
-        self.stderr = Some(PendingEntry::Thunk(|| Ok(OsHandle::stderr())));
+        self.stderr = Some(PendingEntry::Thunk(Stderr::stderr));
         self
     }
 
     /// Inherit the stdin, stdout, and stderr streams from the host process.
     pub fn inherit_stdio(&mut self) -> &mut Self {
-        self.stdin = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdin())));
-        self.stdout = Some(PendingEntry::Thunk(|| Ok(OsHandle::stdout())));
-        self.stderr = Some(PendingEntry::Thunk(|| Ok(OsHandle::stderr())));
+        self.stdin = Some(PendingEntry::Thunk(Stdin::stdin));
+        self.stdout = Some(PendingEntry::Thunk(Stdout::stdout));
+        self.stderr = Some(PendingEntry::Thunk(Stderr::stderr));
         self
     }
 
@@ -231,28 +249,32 @@ impl WasiCtxBuilder {
 
     /// Provide a File to use as stdin
     pub fn stdin(&mut self, file: File) -> &mut Self {
-        self.stdin = Some(PendingEntry::File(file));
+        self.stdin = Some(PendingEntry::OsHandle(file));
         self
     }
 
     /// Provide a File to use as stdout
     pub fn stdout(&mut self, file: File) -> &mut Self {
-        self.stdout = Some(PendingEntry::File(file));
+        self.stdout = Some(PendingEntry::OsHandle(file));
         self
     }
 
     /// Provide a File to use as stderr
     pub fn stderr(&mut self, file: File) -> &mut Self {
-        self.stderr = Some(PendingEntry::File(file));
+        self.stderr = Some(PendingEntry::OsHandle(file));
         self
     }
 
     /// Add a preopened directory.
     pub fn preopened_dir<P: AsRef<Path>>(&mut self, dir: File, guest_path: P) -> &mut Self {
-        self.preopens.as_mut().unwrap().push((
-            guest_path.as_ref().to_owned(),
-            Box::new(OsHandle::from(dir)),
-        ));
+        let preopen = PendingPreopen::new(move || {
+            let dir = OsDir::try_from(dir).map_err(WasiCtxBuilderError::from)?;
+            Ok(Box::new(dir))
+        });
+        self.preopens
+            .as_mut()
+            .unwrap()
+            .push((guest_path.as_ref().to_owned(), preopen));
         self
     }
 
@@ -277,18 +299,22 @@ impl WasiCtxBuilder {
             }
         }
 
-        let dir = if let VirtualDirEntry::Directory(entries) = dir {
-            let mut dir = VirtualDir::new(true);
-            populate_directory(entries, &mut dir);
-            Box::new(dir)
-        } else {
-            panic!("the root of a VirtualDirEntry tree must be a VirtualDirEntry::Directory");
-        };
-
+        let guest_path_owned = guest_path.as_ref().to_owned();
+        let preopen = PendingPreopen::new(move || {
+            if let VirtualDirEntry::Directory(entries) = dir {
+                let mut dir = VirtualDir::new(true);
+                populate_directory(entries, &mut dir);
+                Ok(Box::new(dir))
+            } else {
+                Err(WasiCtxBuilderError::VirtualDirEntryRootNotADirectory(
+                    guest_path_owned,
+                ))
+            }
+        });
         self.preopens
             .as_mut()
             .unwrap()
-            .push((guest_path.as_ref().to_owned(), dir));
+            .push((guest_path.as_ref().to_owned(), preopen));
         self
     }
 
@@ -336,15 +362,16 @@ impl WasiCtxBuilder {
             log::debug!("WasiCtx inserting entry {:?}", pending);
             let fd = match pending {
                 PendingEntry::Thunk(f) => {
-                    let handle = EntryHandle::new(f()?);
-                    let entry = Entry::from(handle)?;
+                    let handle = EntryHandle::from(f()?);
+                    let entry = Entry::new(handle);
                     entries
                         .insert(entry)
                         .ok_or(WasiCtxBuilderError::TooManyFilesOpen)?
                 }
-                PendingEntry::File(f) => {
-                    let handle = EntryHandle::new(OsHandle::from(f));
-                    let entry = Entry::from(handle)?;
+                PendingEntry::OsHandle(f) => {
+                    let handle = OsOther::try_from(f)?;
+                    let handle = EntryHandle::new(handle);
+                    let entry = Entry::new(handle);
                     entries
                         .insert(entry)
                         .ok_or(WasiCtxBuilderError::TooManyFilesOpen)?
@@ -353,13 +380,9 @@ impl WasiCtxBuilder {
             log::debug!("WasiCtx inserted at {:?}", fd);
         }
         // Then add the preopen entries.
-        for (guest_path, dir) in self.preopens.take().unwrap() {
-            if !dir.is_directory() {
-                return Err(WasiCtxBuilderError::NotADirectory(guest_path));
-            }
-
-            let handle = EntryHandle::from(dir);
-            let mut entry = Entry::from(handle)?;
+        for (guest_path, preopen) in self.preopens.take().unwrap() {
+            let handle = EntryHandle::from(preopen.into()?);
+            let mut entry = Entry::new(handle);
             entry.preopen_path = Some(guest_path);
             let fd = entries
                 .insert(entry)
diff --git a/crates/wasi-common/src/entry.rs b/crates/wasi-common/src/entry.rs
index debd90461..eb311a7b1 100644
--- a/crates/wasi-common/src/entry.rs
+++ b/crates/wasi-common/src/entry.rs
@@ -1,11 +1,9 @@
-use crate::handle::Handle;
-use crate::wasi::types::{Filetype, Rights};
+use crate::handle::{Handle, HandleRights};
+use crate::wasi::types::Filetype;
 use crate::wasi::{Errno, Result};
-use std::cell::Cell;
 use std::ops::Deref;
 use std::path::PathBuf;
 use std::rc::Rc;
-use std::{fmt, io};
 
 pub(crate) struct EntryHandle(Rc<dyn Handle>);
 
@@ -33,118 +31,67 @@ impl Deref for EntryHandle {
     }
 }
 
-/// An abstraction struct serving as a wrapper for a host `Descriptor` object which requires
-/// certain rights `rights` in order to be accessed correctly.
+/// An abstraction struct serving as a wrapper for a `Handle` object.
 ///
-/// Here, the `descriptor` field stores the host `Descriptor` object (such as a file descriptor, or
-/// stdin handle), and accessing it can only be done via the provided `Entry::as_descriptor` method
+/// Here, the `handle` field stores an instance of `Handle` type (such as a file descriptor, or
+/// stdin handle), and accessing it can only be done via the provided `Entry::as_handle` method
 /// which require a set of base and inheriting rights to be specified, verifying whether the stored
-/// `Descriptor` object is valid for the rights specified.
+/// `Handle` object is valid for the rights specified.
 pub(crate) struct Entry {
-    pub(crate) file_type: Filetype,
     handle: EntryHandle,
-    pub(crate) rights: Cell<EntryRights>,
     pub(crate) preopen_path: Option<PathBuf>,
     // TODO: directories
 }
 
-/// Represents rights of an `Entry` entity, either already held or
-/// required.
-#[derive(Debug, Copy, Clone)]
-pub(crate) struct EntryRights {
-    pub(crate) base: Rights,
-    pub(crate) inheriting: Rights,
-}
-
-impl EntryRights {
-    pub(crate) fn new(base: Rights, inheriting: Rights) -> Self {
-        Self { base, inheriting }
-    }
-
-    /// Create new `EntryRights` instance from `base` rights only, keeping
-    /// `inheriting` set to none.
-    pub(crate) fn from_base(base: Rights) -> Self {
-        Self {
-            base,
-            inheriting: Rights::empty(),
-        }
-    }
-
-    /// Create new `EntryRights` instance with both `base` and `inheriting`
-    /// rights set to none.
-    pub(crate) fn empty() -> Self {
+impl Entry {
+    pub(crate) fn new(handle: EntryHandle) -> Self {
+        let preopen_path = None;
         Self {
-            base: Rights::empty(),
-            inheriting: Rights::empty(),
+            handle,
+            preopen_path,
         }
     }
 
-    /// Check if `other` is a subset of those rights.
-    pub(crate) fn contains(&self, other: &Self) -> bool {
-        self.base.contains(&other.base) && self.inheriting.contains(&other.inheriting)
+    pub(crate) fn get_file_type(&self) -> Filetype {
+        self.handle.get_file_type()
     }
-}
 
-impl fmt::Display for EntryRights {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(
-            f,
-            "EntryRights {{ base: {}, inheriting: {} }}",
-            self.base, self.inheriting
-        )
+    pub(crate) fn get_rights(&self) -> HandleRights {
+        self.handle.get_rights()
     }
-}
 
-impl Entry {
-    pub(crate) fn from(handle: EntryHandle) -> io::Result<Self> {
-        let file_type = handle.get_file_type()?;
-        let rights = handle.get_rights()?;
-        Ok(Self {
-            file_type,
-            handle,
-            rights: Cell::new(rights),
-            preopen_path: None,
-        })
+    pub(crate) fn set_rights(&self, rights: HandleRights) {
+        self.handle.set_rights(rights)
     }
 
-    /// Convert this `Entry` into a host `Descriptor` object provided the specified
+    /// Convert this `Entry` into a `Handle` object provided the specified
     /// `rights` rights are set on this `Entry` object.
     ///
-    /// The `Entry` can only be converted into a valid `Descriptor` object if
+    /// The `Entry` can only be converted into a valid `Handle` object if
     /// the specified set of base rights, and inheriting rights encapsulated within `rights`
-    /// `EntryRights` structure is a subset of rights attached to this `Entry`. The check is
+    /// `HandleRights` structure is a subset of rights attached to this `Entry`. The check is
     /// performed using `Entry::validate_rights` method. If the check fails, `Errno::Notcapable`
     /// is returned.
-    pub(crate) fn as_handle(&self, rights: &EntryRights) -> Result<EntryHandle> {
+    pub(crate) fn as_handle(&self, rights: &HandleRights) -> Result<EntryHandle> {
         self.validate_rights(rights)?;
         Ok(self.handle.get())
     }
 
-    /// Check if this `Entry` object satisfies the specified `EntryRights`; i.e., if
+    /// Check if this `Entry` object satisfies the specified `HandleRights`; i.e., if
     /// rights attached to this `Entry` object are a superset.
     ///
     /// Upon unsuccessful check, `Errno::Notcapable` is returned.
-    pub(crate) fn validate_rights(&self, rights: &EntryRights) -> Result<()> {
-        if self.rights.get().contains(rights) {
+    pub(crate) fn validate_rights(&self, rights: &HandleRights) -> Result<()> {
+        let this_rights = self.handle.get_rights();
+        if this_rights.contains(rights) {
             Ok(())
         } else {
             log::trace!(
                 "     | validate_rights failed: required rights = {}; actual rights = {}",
                 rights,
-                self.rights.get(),
+                this_rights,
             );
             Err(Errno::Notcapable)
         }
     }
-
-    /// Test whether this descriptor is considered a tty within WASI.
-    /// Note that since WASI itself lacks an `isatty` syscall and relies
-    /// on a conservative approximation, we use the same approximation here.
-    pub(crate) fn isatty(&self) -> bool {
-        self.file_type == Filetype::CharacterDevice
-            && self
-                .rights
-                .get()
-                .contains(&EntryRights::from_base(Rights::FD_SEEK | Rights::FD_TELL))
-    }
 }
diff --git a/crates/wasi-common/src/handle.rs b/crates/wasi-common/src/handle.rs
index 671ae2d2a..21a0ab4f2 100644
--- a/crates/wasi-common/src/handle.rs
+++ b/crates/wasi-common/src/handle.rs
@@ -1,20 +1,74 @@
-use crate::entry::EntryRights;
-use crate::wasi::{types, Errno, Result};
+use crate::wasi::types::{self, Rights};
+use crate::wasi::{Errno, Result};
 use std::any::Any;
+use std::fmt;
 use std::io::{self, SeekFrom};
 
+/// Represents rights of a `Handle`, either already held or required.
+#[derive(Debug, Copy, Clone)]
+pub(crate) struct HandleRights {
+    pub(crate) base: Rights,
+    pub(crate) inheriting: Rights,
+}
+
+impl HandleRights {
+    pub(crate) fn new(base: Rights, inheriting: Rights) -> Self {
+        Self { base, inheriting }
+    }
+
+    /// Create new `HandleRights` instance from `base` rights only, keeping
+    /// `inheriting` set to none.
+    pub(crate) fn from_base(base: Rights) -> Self {
+        Self {
+            base,
+            inheriting: Rights::empty(),
+        }
+    }
+
+    /// Create new `HandleRights` instance with both `base` and `inheriting`
+    /// rights set to none.
+    pub(crate) fn empty() -> Self {
+        Self {
+            base: Rights::empty(),
+            inheriting: Rights::empty(),
+        }
+    }
+
+    /// Check if `other` is a subset of those rights.
+    pub(crate) fn contains(&self, other: &Self) -> bool {
+        self.base.contains(&other.base) && self.inheriting.contains(&other.inheriting)
+    }
+}
+
+impl fmt::Display for HandleRights {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "HandleRights {{ base: {}, inheriting: {} }}",
+            self.base, self.inheriting
+        )
+    }
+}
+
 pub(crate) trait Handle {
     fn as_any(&self) -> &dyn Any;
     fn try_clone(&self) -> io::Result<Box<dyn Handle>>;
-    fn get_file_type(&self) -> io::Result<types::Filetype>;
-    fn get_rights(&self) -> io::Result<EntryRights> {
-        Ok(EntryRights::empty())
+    fn get_file_type(&self) -> types::Filetype;
+    fn get_rights(&self) -> HandleRights {
+        HandleRights::empty()
     }
+    fn set_rights(&self, rights: HandleRights);
     fn is_directory(&self) -> bool {
-        if let Ok(ft) = self.get_file_type() {
-            return ft == types::Filetype::Directory;
-        }
-        false
+        self.get_file_type() == types::Filetype::Directory
+    }
+    /// Test whether this descriptor is considered a tty within WASI.
+    /// Note that since WASI itself lacks an `isatty` syscall and relies
+    /// on a conservative approximation, we use the same approximation here.
+    fn is_tty(&self) -> bool {
+        let file_type = self.get_file_type();
+        let rights = self.get_rights();
+        let required_rights = HandleRights::from_base(Rights::FD_SEEK | Rights::FD_TELL);
+        file_type == types::Filetype::CharacterDevice && rights.contains(&required_rights)
     }
     // TODO perhaps should be a separate trait?
     // FdOps
@@ -73,7 +127,7 @@ pub(crate) trait Handle {
     fn sync(&self) -> Result<()> {
         Ok(())
     }
-    fn write_vectored(&self, _iovs: &[io::IoSlice], _isatty: bool) -> Result<usize> {
+    fn write_vectored(&self, _iovs: &[io::IoSlice]) -> Result<usize> {
         Err(Errno::Badf)
     }
     // TODO perhaps should be a separate trait?
diff --git a/crates/wasi-common/src/path.rs b/crates/wasi-common/src/path.rs
index 515ed1801..f5399a7b7 100644
--- a/crates/wasi-common/src/path.rs
+++ b/crates/wasi-common/src/path.rs
@@ -1,5 +1,5 @@
-use crate::entry::{Entry, EntryRights};
-use crate::handle::Handle;
+use crate::entry::Entry;
+use crate::handle::{Handle, HandleRights};
 use crate::wasi::{types, Errno, Result};
 use std::path::{Component, Path};
 use std::str;
@@ -12,7 +12,7 @@ pub(crate) use crate::sys::path::{from_host, open_rights};
 /// This is a workaround for not having Capsicum support in the OS.
 pub(crate) fn get(
     entry: &Entry,
-    required_rights: &EntryRights,
+    required_rights: &HandleRights,
     dirflags: types::Lookupflags,
     path: &GuestPtr<'_, str>,
     needs_final_component: bool,
@@ -33,7 +33,7 @@ pub(crate) fn get(
         return Err(Errno::Ilseq);
     }
 
-    if entry.file_type != types::Filetype::Directory {
+    if entry.get_file_type() != types::Filetype::Directory {
         // if `dirfd` doesn't refer to a directory, return `Notdir`.
         return Err(Errno::Notdir);
     }
diff --git a/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs b/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs
index 23649b20b..4ab53f5d6 100644
--- a/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs
+++ b/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs
@@ -1,4 +1,5 @@
-use crate::entry::{Entry, EntryHandle, EntryRights};
+use crate::entry::{Entry, EntryHandle};
+use crate::handle::HandleRights;
 use crate::sys::clock;
 use crate::wasi::wasi_snapshot_preview1::WasiSnapshotPreview1;
 use crate::wasi::{types, AsBytes, Errno, Result};
@@ -91,7 +92,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         len: types::Filesize,
         advice: types::Advice,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_ADVISE);
+        let required_rights = HandleRights::from_base(types::Rights::FD_ADVISE);
         let entry = self.get_entry(fd)?;
         entry
             .as_handle(&required_rights)?
@@ -104,7 +105,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         offset: types::Filesize,
         len: types::Filesize,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_ALLOCATE);
+        let required_rights = HandleRights::from_base(types::Rights::FD_ALLOCATE);
         let entry = self.get_entry(fd)?;
         entry.as_handle(&required_rights)?.allocate(offset, len)
     }
@@ -121,18 +122,18 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
     }
 
     fn fd_datasync(&self, fd: types::Fd) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_DATASYNC);
+        let required_rights = HandleRights::from_base(types::Rights::FD_DATASYNC);
         let entry = self.get_entry(fd)?;
         entry.as_handle(&required_rights)?.datasync()
     }
 
     fn fd_fdstat_get(&self, fd: types::Fd) -> Result<types::Fdstat> {
         let entry = self.get_entry(fd)?;
-        let file = entry.as_handle(&EntryRights::empty())?;
+        let file = entry.as_handle(&HandleRights::empty())?;
         let fs_flags = file.fdstat_get()?;
-        let rights = entry.rights.get();
+        let rights = entry.get_rights();
         let fdstat = types::Fdstat {
-            fs_filetype: entry.file_type,
+            fs_filetype: entry.get_file_type(),
             fs_rights_base: rights.base,
             fs_rights_inheriting: rights.inheriting,
             fs_flags,
@@ -141,7 +142,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
     }
 
     fn fd_fdstat_set_flags(&self, fd: types::Fd, flags: types::Fdflags) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_FDSTAT_SET_FLAGS);
+        let required_rights = HandleRights::from_base(types::Rights::FD_FDSTAT_SET_FLAGS);
         let entry = self.get_entry(fd)?;
         entry.as_handle(&required_rights)?.fdstat_set_flags(flags)
     }
@@ -152,24 +153,24 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         fs_rights_base: types::Rights,
         fs_rights_inheriting: types::Rights,
     ) -> Result<()> {
-        let rights = EntryRights::new(fs_rights_base, fs_rights_inheriting);
+        let rights = HandleRights::new(fs_rights_base, fs_rights_inheriting);
         let entry = self.get_entry(fd)?;
-        if !entry.rights.get().contains(&rights) {
+        if !entry.get_rights().contains(&rights) {
             return Err(Errno::Notcapable);
         }
-        entry.rights.set(rights);
+        entry.set_rights(rights);
         Ok(())
     }
 
     fn fd_filestat_get(&self, fd: types::Fd) -> Result<types::Filestat> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_FILESTAT_GET);
+        let required_rights = HandleRights::from_base(types::Rights::FD_FILESTAT_GET);
         let entry = self.get_entry(fd)?;
         let host_filestat = entry.as_handle(&required_rights)?.filestat_get()?;
         Ok(host_filestat)
     }
 
     fn fd_filestat_set_size(&self, fd: types::Fd, size: types::Filesize) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_FILESTAT_SET_SIZE);
+        let required_rights = HandleRights::from_base(types::Rights::FD_FILESTAT_SET_SIZE);
         let entry = self.get_entry(fd)?;
         // This check will be unnecessary when rust-lang/rust#63326 is fixed
         if size > i64::max_value() as u64 {
@@ -185,7 +186,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         mtim: types::Timestamp,
         fst_flags: types::Fstflags,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_FILESTAT_SET_TIMES);
+        let required_rights = HandleRights::from_base(types::Rights::FD_FILESTAT_SET_TIMES);
         let entry = self.get_entry(fd)?;
         entry
             .as_handle(&required_rights)?
@@ -213,7 +214,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         }
 
         let required_rights =
-            EntryRights::from_base(types::Rights::FD_READ | types::Rights::FD_SEEK);
+            HandleRights::from_base(types::Rights::FD_READ | types::Rights::FD_SEEK);
         let entry = self.get_entry(fd)?;
         if offset > i64::max_value() as u64 {
             return Err(Errno::Io);
@@ -227,9 +228,9 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
 
     fn fd_prestat_get(&self, fd: types::Fd) -> Result<types::Prestat> {
         // TODO: should we validate any rights here?
-        let fe = self.get_entry(fd)?;
-        let po_path = fe.preopen_path.as_ref().ok_or(Errno::Notsup)?;
-        if fe.file_type != types::Filetype::Directory {
+        let entry = self.get_entry(fd)?;
+        let po_path = entry.preopen_path.as_ref().ok_or(Errno::Notsup)?;
+        if entry.get_file_type() != types::Filetype::Directory {
             return Err(Errno::Notdir);
         }
 
@@ -247,9 +248,9 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         path_len: types::Size,
     ) -> Result<()> {
         // TODO: should we validate any rights here?
-        let fe = self.get_entry(fd)?;
-        let po_path = fe.preopen_path.as_ref().ok_or(Errno::Notsup)?;
-        if fe.file_type != types::Filetype::Directory {
+        let entry = self.get_entry(fd)?;
+        let po_path = entry.preopen_path.as_ref().ok_or(Errno::Notsup)?;
+        if entry.get_file_type() != types::Filetype::Directory {
             return Err(Errno::Notdir);
         }
 
@@ -289,7 +290,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         }
 
         let required_rights =
-            EntryRights::from_base(types::Rights::FD_WRITE | types::Rights::FD_SEEK);
+            HandleRights::from_base(types::Rights::FD_WRITE | types::Rights::FD_SEEK);
         let entry = self.get_entry(fd)?;
 
         if offset > i64::max_value() as u64 {
@@ -318,7 +319,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
             slices.push(io::IoSliceMut::new(slice));
         }
 
-        let required_rights = EntryRights::from_base(types::Rights::FD_READ);
+        let required_rights = HandleRights::from_base(types::Rights::FD_READ);
         let entry = self.get_entry(fd)?;
         let host_nread = entry
             .as_handle(&required_rights)?
@@ -335,7 +336,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         buf_len: types::Size,
         cookie: types::Dircookie,
     ) -> Result<types::Size> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_READDIR);
+        let required_rights = HandleRights::from_base(types::Rights::FD_READDIR);
         let entry = self.get_entry(fd)?;
 
         let mut bufused = 0;
@@ -395,7 +396,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         } else {
             types::Rights::FD_SEEK | types::Rights::FD_TELL
         };
-        let required_rights = EntryRights::from_base(base);
+        let required_rights = HandleRights::from_base(base);
         let entry = self.get_entry(fd)?;
         let pos = match whence {
             types::Whence::Cur => SeekFrom::Current(offset),
@@ -407,13 +408,13 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
     }
 
     fn fd_sync(&self, fd: types::Fd) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_SYNC);
+        let required_rights = HandleRights::from_base(types::Rights::FD_SYNC);
         let entry = self.get_entry(fd)?;
         entry.as_handle(&required_rights)?.sync()
     }
 
     fn fd_tell(&self, fd: types::Fd) -> Result<types::Filesize> {
-        let required_rights = EntryRights::from_base(types::Rights::FD_TELL);
+        let required_rights = HandleRights::from_base(types::Rights::FD_TELL);
         let entry = self.get_entry(fd)?;
         let host_offset = entry
             .as_handle(&required_rights)?
@@ -435,19 +436,19 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
             };
             slices.push(io::IoSlice::new(slice));
         }
-        let required_rights = EntryRights::from_base(types::Rights::FD_WRITE);
+        let required_rights = HandleRights::from_base(types::Rights::FD_WRITE);
         let entry = self.get_entry(fd)?;
-        let isatty = entry.isatty();
         let host_nwritten = entry
             .as_handle(&required_rights)?
-            .write_vectored(&slices, isatty)?
+            .write_vectored(&slices)?
             .try_into()?;
         Ok(host_nwritten)
     }
 
     fn path_create_directory(&self, dirfd: types::Fd, path: &GuestPtr<'_, str>) -> Result<()> {
-        let required_rights =
-            EntryRights::from_base(types::Rights::PATH_OPEN | types::Rights::PATH_CREATE_DIRECTORY);
+        let required_rights = HandleRights::from_base(
+            types::Rights::PATH_OPEN | types::Rights::PATH_CREATE_DIRECTORY,
+        );
         let entry = self.get_entry(dirfd)?;
         let (dirfd, path) = path::get(
             &entry,
@@ -465,7 +466,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         flags: types::Lookupflags,
         path: &GuestPtr<'_, str>,
     ) -> Result<types::Filestat> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_FILESTAT_GET);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_FILESTAT_GET);
         let entry = self.get_entry(dirfd)?;
         let (dirfd, path) = path::get(&entry, &required_rights, flags, path, false)?;
         let host_filestat = dirfd
@@ -489,7 +490,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         mtim: types::Timestamp,
         fst_flags: types::Fstflags,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_FILESTAT_SET_TIMES);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_FILESTAT_SET_TIMES);
         let entry = self.get_entry(dirfd)?;
         let (dirfd, path) = path::get(&entry, &required_rights, flags, path, false)?;
         dirfd
@@ -512,7 +513,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         new_fd: types::Fd,
         new_path: &GuestPtr<'_, str>,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_LINK_SOURCE);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_LINK_SOURCE);
         let old_entry = self.get_entry(old_fd)?;
         let (old_dirfd, old_path) = path::get(
             &old_entry,
@@ -521,7 +522,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
             old_path,
             false,
         )?;
-        let required_rights = EntryRights::from_base(types::Rights::PATH_LINK_TARGET);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_LINK_TARGET);
         let new_entry = self.get_entry(new_fd)?;
         let (new_dirfd, new_path) = path::get(
             &new_entry,
@@ -549,7 +550,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         fdflags: types::Fdflags,
     ) -> Result<types::Fd> {
         let needed_rights = path::open_rights(
-            &EntryRights::new(fs_rights_base, fs_rights_inheriting),
+            &HandleRights::new(fs_rights_base, fs_rights_inheriting),
             oflags,
             fdflags,
         );
@@ -577,14 +578,14 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
             write
         );
         let fd = dirfd.openat(&path, read, write, oflags, fdflags)?;
-        let fe = Entry::from(EntryHandle::from(fd))?;
+        let entry = Entry::new(EntryHandle::from(fd));
         // We need to manually deny the rights which are not explicitly requested
         // because Entry::from will assign maximal consistent rights.
-        let mut rights = fe.rights.get();
+        let mut rights = entry.get_rights();
         rights.base &= fs_rights_base;
         rights.inheriting &= fs_rights_inheriting;
-        fe.rights.set(rights);
-        let guest_fd = self.insert_entry(fe)?;
+        entry.set_rights(rights);
+        let guest_fd = self.insert_entry(entry)?;
         Ok(guest_fd)
     }
 
@@ -595,7 +596,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         buf: &GuestPtr<u8>,
         buf_len: types::Size,
     ) -> Result<types::Size> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_READLINK);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_READLINK);
         let entry = self.get_entry(dirfd)?;
         let (dirfd, path) = path::get(
             &entry,
@@ -615,7 +616,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
     }
 
     fn path_remove_directory(&self, dirfd: types::Fd, path: &GuestPtr<'_, str>) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_REMOVE_DIRECTORY);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_REMOVE_DIRECTORY);
         let entry = self.get_entry(dirfd)?;
         let (dirfd, path) = path::get(
             &entry,
@@ -634,7 +635,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         new_fd: types::Fd,
         new_path: &GuestPtr<'_, str>,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_RENAME_SOURCE);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_RENAME_SOURCE);
         let entry = self.get_entry(old_fd)?;
         let (old_dirfd, old_path) = path::get(
             &entry,
@@ -643,7 +644,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
             old_path,
             true,
         )?;
-        let required_rights = EntryRights::from_base(types::Rights::PATH_RENAME_TARGET);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_RENAME_TARGET);
         let entry = self.get_entry(new_fd)?;
         let (new_dirfd, new_path) = path::get(
             &entry,
@@ -661,7 +662,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
         dirfd: types::Fd,
         new_path: &GuestPtr<'_, str>,
     ) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_SYMLINK);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_SYMLINK);
         let entry = self.get_entry(dirfd)?;
         let (new_fd, new_path) = path::get(
             &entry,
@@ -680,7 +681,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
     }
 
     fn path_unlink_file(&self, dirfd: types::Fd, path: &GuestPtr<'_, str>) -> Result<()> {
-        let required_rights = EntryRights::from_base(types::Rights::PATH_UNLINK_FILE);
+        let required_rights = HandleRights::from_base(types::Rights::PATH_UNLINK_FILE);
         let entry = self.get_entry(dirfd)?;
         let (dirfd, path) = path::get(
             &entry,
@@ -740,7 +741,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
                 }
                 types::SubscriptionU::FdRead(fd_read) => {
                     let fd = fd_read.file_descriptor;
-                    let required_rights = EntryRights::from_base(
+                    let required_rights = HandleRights::from_base(
                         types::Rights::FD_READ | types::Rights::POLL_FD_READWRITE,
                     );
                     let entry = match self.get_entry(fd) {
@@ -766,7 +767,7 @@ impl<'a> WasiSnapshotPreview1 for WasiCtx {
                 }
                 types::SubscriptionU::FdWrite(fd_write) => {
                     let fd = fd_write.file_descriptor;
-                    let required_rights = EntryRights::from_base(
+                    let required_rights = HandleRights::from_base(
                         types::Rights::FD_WRITE | types::Rights::POLL_FD_READWRITE,
                     );
                     let entry = match self.get_entry(fd) {
diff --git a/crates/wasi-common/src/sys/mod.rs b/crates/wasi-common/src/sys/mod.rs
index 61ed66700..bca99f146 100644
--- a/crates/wasi-common/src/sys/mod.rs
+++ b/crates/wasi-common/src/sys/mod.rs
@@ -1,6 +1,9 @@
 pub(crate) mod clock;
 pub(crate) mod fd;
-pub(crate) mod oshandle;
+pub(crate) mod osdir;
+pub(crate) mod osfile;
+pub(crate) mod osother;
+pub(crate) mod stdio;
 
 use cfg_if::cfg_if;
 
@@ -20,3 +23,65 @@ cfg_if! {
 
 pub(crate) use sys_impl::path;
 pub(crate) use sys_impl::poll;
+
+use super::handle::Handle;
+use crate::wasi::types;
+use osdir::OsDir;
+use osfile::OsFile;
+use osother::OsOther;
+use std::convert::TryFrom;
+use std::fs::File;
+use std::io;
+use std::mem::ManuallyDrop;
+use stdio::{Stderr, Stdin, Stdout};
+use sys_impl::get_file_type;
+
+pub(crate) trait AsFile {
+    fn as_file(&self) -> io::Result<ManuallyDrop<File>>;
+}
+
+impl AsFile for dyn Handle + 'static {
+    fn as_file(&self) -> io::Result<ManuallyDrop<File>> {
+        if let Some(file) = self.as_any().downcast_ref::<OsFile>() {
+            file.as_file()
+        } else if let Some(dir) = self.as_any().downcast_ref::<OsDir>() {
+            dir.as_file()
+        } else if let Some(stdin) = self.as_any().downcast_ref::<Stdin>() {
+            stdin.as_file()
+        } else if let Some(stdout) = self.as_any().downcast_ref::<Stdout>() {
+            stdout.as_file()
+        } else if let Some(stderr) = self.as_any().downcast_ref::<Stderr>() {
+            stderr.as_file()
+        } else if let Some(other) = self.as_any().downcast_ref::<OsOther>() {
+            other.as_file()
+        } else {
+            log::error!("tried to make std::fs::File from non-OS handle");
+            Err(io::Error::from_raw_os_error(libc::EBADF))
+        }
+    }
+}
+
+impl TryFrom<File> for Box<dyn Handle> {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let file_type = get_file_type(&file)?;
+        match file_type {
+            types::Filetype::RegularFile => {
+                let handle = OsFile::try_from(file)?;
+                log::debug!("Created new instance of OsFile: {:?}", handle);
+                Ok(Box::new(handle))
+            }
+            types::Filetype::Directory => {
+                let handle = OsDir::try_from(file)?;
+                log::debug!("Created new instance of OsDir: {:?}", handle);
+                Ok(Box::new(handle))
+            }
+            _ => {
+                let handle = OsOther::try_from(file)?;
+                log::debug!("Created new instance of OsOther: {:?}", handle);
+                Ok(Box::new(handle))
+            }
+        }
+    }
+}
diff --git a/crates/wasi-common/src/sys/osdir.rs b/crates/wasi-common/src/sys/osdir.rs
new file mode 100644
index 000000000..29a69e0c1
--- /dev/null
+++ b/crates/wasi-common/src/sys/osdir.rs
@@ -0,0 +1,129 @@
+use super::sys_impl::oshandle::RawOsHandle;
+use super::{fd, path, AsFile};
+use crate::handle::{Handle, HandleRights};
+use crate::wasi::{types, Errno, Result};
+use log::{debug, error};
+use std::any::Any;
+use std::io;
+use std::ops::Deref;
+
+// TODO could this be cleaned up?
+// The actual `OsDir` struct is OS-dependent, therefore we delegate
+// its definition to OS-specific modules.
+pub(crate) use super::sys_impl::osdir::OsDir;
+
+impl Deref for OsDir {
+    type Target = RawOsHandle;
+
+    fn deref(&self) -> &Self::Target {
+        &self.handle
+    }
+}
+
+impl Handle for OsDir {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
+        let handle = self.handle.try_clone()?;
+        let new = Self::new(self.rights.get(), handle)?;
+        Ok(Box::new(new))
+    }
+    fn get_file_type(&self) -> types::Filetype {
+        types::Filetype::Directory
+    }
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, rights: HandleRights) {
+        self.rights.set(rights)
+    }
+    // FdOps
+    fn fdstat_get(&self) -> Result<types::Fdflags> {
+        fd::fdstat_get(&*self.as_file()?)
+    }
+    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
+        if let Some(new_file) = fd::fdstat_set_flags(&*self.as_file()?, fdflags)? {
+            self.handle.update_from(new_file);
+        }
+        Ok(())
+    }
+    fn filestat_get(&self) -> Result<types::Filestat> {
+        fd::filestat_get(&*self.as_file()?)
+    }
+    fn filestat_set_times(
+        &self,
+        atim: types::Timestamp,
+        mtim: types::Timestamp,
+        fst_flags: types::Fstflags,
+    ) -> Result<()> {
+        fd::filestat_set_times(&*self.as_file()?, atim, mtim, fst_flags)
+    }
+    fn readdir<'a>(
+        &'a self,
+        cookie: types::Dircookie,
+    ) -> Result<Box<dyn Iterator<Item = Result<(types::Dirent, String)>> + 'a>> {
+        fd::readdir(self, cookie)
+    }
+    // PathOps
+    fn create_directory(&self, path: &str) -> Result<()> {
+        path::create_directory(self, path)
+    }
+    fn openat(
+        &self,
+        path: &str,
+        read: bool,
+        write: bool,
+        oflags: types::Oflags,
+        fd_flags: types::Fdflags,
+    ) -> Result<Box<dyn Handle>> {
+        path::open(self, path, read, write, oflags, fd_flags)
+    }
+    fn link(
+        &self,
+        old_path: &str,
+        new_handle: Box<dyn Handle>,
+        new_path: &str,
+        follow: bool,
+    ) -> Result<()> {
+        let new_handle = match new_handle.as_any().downcast_ref::<Self>() {
+            None => {
+                error!("Tried to link with handle that's not an OsDir");
+                return Err(Errno::Badf);
+            }
+            Some(handle) => handle,
+        };
+        path::link(self, old_path, new_handle, new_path, follow)
+    }
+    fn symlink(&self, old_path: &str, new_path: &str) -> Result<()> {
+        path::symlink(old_path, self, new_path)
+    }
+    fn readlink(&self, path: &str, buf: &mut [u8]) -> Result<usize> {
+        path::readlink(self, path, buf)
+    }
+    fn readlinkat(&self, path: &str) -> Result<String> {
+        path::readlinkat(self, path)
+    }
+    fn rename(&self, old_path: &str, new_handle: Box<dyn Handle>, new_path: &str) -> Result<()> {
+        let new_handle = match new_handle.as_any().downcast_ref::<Self>() {
+            None => {
+                error!("Tried to rename with handle that's not an OsDir");
+                return Err(Errno::Badf);
+            }
+            Some(handle) => handle,
+        };
+        debug!("rename (old_dirfd, old_path)=({:?}, {:?})", self, old_path);
+        debug!(
+            "rename (new_dirfd, new_path)=({:?}, {:?})",
+            new_handle, new_path
+        );
+        path::rename(self, old_path, new_handle, new_path)
+    }
+    fn remove_directory(&self, path: &str) -> Result<()> {
+        debug!("remove_directory (dirfd, path)=({:?}, {:?})", self, path);
+        path::remove_directory(self, path)
+    }
+    fn unlink_file(&self, path: &str) -> Result<()> {
+        path::unlink_file(self, path)
+    }
+}
diff --git a/crates/wasi-common/src/sys/osfile.rs b/crates/wasi-common/src/sys/osfile.rs
new file mode 100644
index 000000000..a3491f543
--- /dev/null
+++ b/crates/wasi-common/src/sys/osfile.rs
@@ -0,0 +1,133 @@
+use super::sys_impl::oshandle::RawOsHandle;
+use super::{fd, AsFile};
+use crate::handle::{Handle, HandleRights};
+use crate::wasi::{types, Errno, Result};
+use std::any::Any;
+use std::cell::Cell;
+use std::fs::File;
+use std::io::{self, Read, Seek, SeekFrom, Write};
+use std::ops::Deref;
+
+#[derive(Debug)]
+pub(crate) struct OsFile {
+    rights: Cell<HandleRights>,
+    handle: RawOsHandle,
+}
+
+impl OsFile {
+    pub(super) fn new(rights: HandleRights, handle: RawOsHandle) -> Self {
+        let rights = Cell::new(rights);
+        Self { rights, handle }
+    }
+}
+
+impl Deref for OsFile {
+    type Target = RawOsHandle;
+
+    fn deref(&self) -> &Self::Target {
+        &self.handle
+    }
+}
+
+impl Handle for OsFile {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
+        let handle = self.handle.try_clone()?;
+        let rights = self.rights.clone();
+        Ok(Box::new(Self { rights, handle }))
+    }
+    fn get_file_type(&self) -> types::Filetype {
+        types::Filetype::RegularFile
+    }
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, rights: HandleRights) {
+        self.rights.set(rights)
+    }
+    // FdOps
+    fn advise(
+        &self,
+        advice: types::Advice,
+        offset: types::Filesize,
+        len: types::Filesize,
+    ) -> Result<()> {
+        fd::advise(self, advice, offset, len)
+    }
+    fn allocate(&self, offset: types::Filesize, len: types::Filesize) -> Result<()> {
+        let fd = self.as_file()?;
+        let metadata = fd.metadata()?;
+        let current_size = metadata.len();
+        let wanted_size = offset.checked_add(len).ok_or(Errno::TooBig)?;
+        // This check will be unnecessary when rust-lang/rust#63326 is fixed
+        if wanted_size > i64::max_value() as u64 {
+            return Err(Errno::TooBig);
+        }
+        if wanted_size > current_size {
+            fd.set_len(wanted_size)?;
+        }
+        Ok(())
+    }
+    fn datasync(&self) -> Result<()> {
+        self.as_file()?.sync_data()?;
+        Ok(())
+    }
+    fn fdstat_get(&self) -> Result<types::Fdflags> {
+        fd::fdstat_get(&*self.as_file()?)
+    }
+    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
+        if let Some(new_handle) = fd::fdstat_set_flags(&*self.as_file()?, fdflags)? {
+            self.handle.update_from(new_handle);
+        }
+        Ok(())
+    }
+    fn filestat_get(&self) -> Result<types::Filestat> {
+        fd::filestat_get(&*self.as_file()?)
+    }
+    fn filestat_set_size(&self, size: types::Filesize) -> Result<()> {
+        self.as_file()?.set_len(size)?;
+        Ok(())
+    }
+    fn filestat_set_times(
+        &self,
+        atim: types::Timestamp,
+        mtim: types::Timestamp,
+        fst_flags: types::Fstflags,
+    ) -> Result<()> {
+        fd::filestat_set_times(&*self.as_file()?, atim, mtim, fst_flags)
+    }
+    fn preadv(&self, buf: &mut [io::IoSliceMut], offset: u64) -> Result<usize> {
+        let mut fd: &File = &*self.as_file()?;
+        let cur_pos = fd.seek(SeekFrom::Current(0))?;
+        fd.seek(SeekFrom::Start(offset))?;
+        let nread = self.read_vectored(buf)?;
+        fd.seek(SeekFrom::Start(cur_pos))?;
+        Ok(nread)
+    }
+    fn pwritev(&self, buf: &[io::IoSlice], offset: u64) -> Result<usize> {
+        let mut fd: &File = &*self.as_file()?;
+        let cur_pos = fd.seek(SeekFrom::Current(0))?;
+        fd.seek(SeekFrom::Start(offset))?;
+        let nwritten = self.write_vectored(&buf)?;
+        fd.seek(SeekFrom::Start(cur_pos))?;
+        Ok(nwritten)
+    }
+    fn read_vectored(&self, iovs: &mut [io::IoSliceMut]) -> Result<usize> {
+        let nread = self.as_file()?.read_vectored(iovs)?;
+        Ok(nread)
+    }
+    fn seek(&self, offset: SeekFrom) -> Result<u64> {
+        let pos = self.as_file()?.seek(offset)?;
+        Ok(pos)
+    }
+    fn sync(&self) -> Result<()> {
+        self.as_file()?.sync_all()?;
+        Ok(())
+    }
+    fn write_vectored(&self, iovs: &[io::IoSlice]) -> Result<usize> {
+        let nwritten = self.as_file()?.write_vectored(&iovs)?;
+        Ok(nwritten)
+    }
+}
diff --git a/crates/wasi-common/src/sys/oshandle.rs b/crates/wasi-common/src/sys/oshandle.rs
deleted file mode 100644
index eeff049cc..000000000
--- a/crates/wasi-common/src/sys/oshandle.rs
+++ /dev/null
@@ -1,277 +0,0 @@
-use super::{fd, path};
-use crate::entry::EntryRights;
-use crate::handle::Handle;
-use crate::sandboxed_tty_writer::SandboxedTTYWriter;
-use crate::wasi::{types, Errno, Result};
-use log::{debug, error};
-use std::any::Any;
-use std::fs::File;
-use std::io::{self, Read, Seek, SeekFrom, Write};
-use std::mem::ManuallyDrop;
-
-pub(crate) use super::sys_impl::oshandle::*;
-
-#[derive(Debug)]
-pub(crate) enum OsHandle {
-    OsFile(OsFile),
-    Stdin,
-    Stdout,
-    Stderr,
-}
-
-impl OsHandle {
-    pub(crate) fn as_os_file(&self) -> Result<&OsFile> {
-        match self {
-            Self::OsFile(fd) => Ok(fd),
-            _ => Err(Errno::Badf),
-        }
-    }
-
-    pub(crate) fn stdin() -> Self {
-        Self::Stdin
-    }
-
-    pub(crate) fn stdout() -> Self {
-        Self::Stdout
-    }
-
-    pub(crate) fn stderr() -> Self {
-        Self::Stderr
-    }
-}
-
-pub(crate) trait AsFile {
-    fn as_file(&self) -> ManuallyDrop<File>;
-}
-
-pub(crate) trait OsHandleExt: Sized {
-    /// Returns the file type.
-    fn get_file_type(&self) -> io::Result<types::Filetype>;
-    /// Returns the set of all possible rights that are both relevant for the file
-    /// type and consistent with the open mode.
-    fn get_rights(&self, filetype: types::Filetype) -> io::Result<EntryRights>;
-    fn from_null() -> io::Result<Self>;
-}
-
-impl From<OsFile> for OsHandle {
-    fn from(file: OsFile) -> Self {
-        Self::OsFile(file)
-    }
-}
-
-impl Handle for OsHandle {
-    fn as_any(&self) -> &dyn Any {
-        self
-    }
-    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
-        let new_handle = match self {
-            Self::OsFile(file) => Self::OsFile(file.try_clone()?),
-            Self::Stdin => Self::Stdin,
-            Self::Stdout => Self::Stdout,
-            Self::Stderr => Self::Stderr,
-        };
-        Ok(Box::new(new_handle))
-    }
-    fn get_file_type(&self) -> io::Result<types::Filetype> {
-        <Self as OsHandleExt>::get_file_type(self)
-    }
-
-    fn get_rights(&self) -> io::Result<EntryRights> {
-        <Self as OsHandleExt>::get_rights(self, <Self as Handle>::get_file_type(self)?)
-    }
-    // FdOps
-    fn advise(
-        &self,
-        advice: types::Advice,
-        offset: types::Filesize,
-        len: types::Filesize,
-    ) -> Result<()> {
-        fd::advise(self.as_os_file()?, advice, offset, len)
-    }
-    fn allocate(&self, offset: types::Filesize, len: types::Filesize) -> Result<()> {
-        let fd = self.as_file();
-        let metadata = fd.metadata()?;
-        let current_size = metadata.len();
-        let wanted_size = offset.checked_add(len).ok_or(Errno::TooBig)?;
-        // This check will be unnecessary when rust-lang/rust#63326 is fixed
-        if wanted_size > i64::max_value() as u64 {
-            return Err(Errno::TooBig);
-        }
-        if wanted_size > current_size {
-            fd.set_len(wanted_size)?;
-        }
-        Ok(())
-    }
-    fn datasync(&self) -> Result<()> {
-        self.as_file().sync_data()?;
-        Ok(())
-    }
-    fn fdstat_get(&self) -> Result<types::Fdflags> {
-        fd::fdstat_get(&self.as_file())
-    }
-    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
-        if let Some(new_file) = fd::fdstat_set_flags(&self.as_file(), fdflags)? {
-            // If we don't deal with OsFile, then something went wrong, and we
-            // should fail. On the other hand, is that even possible?
-            self.as_os_file()?.update_from(new_file);
-        }
-        Ok(())
-    }
-    fn filestat_get(&self) -> Result<types::Filestat> {
-        fd::filestat_get(&self.as_file())
-    }
-    fn filestat_set_size(&self, size: types::Filesize) -> Result<()> {
-        self.as_os_file()?.as_file().set_len(size)?;
-        Ok(())
-    }
-    fn filestat_set_times(
-        &self,
-        atim: types::Timestamp,
-        mtim: types::Timestamp,
-        fst_flags: types::Fstflags,
-    ) -> Result<()> {
-        fd::filestat_set_times(&self.as_file(), atim, mtim, fst_flags)
-    }
-    fn preadv(&self, buf: &mut [io::IoSliceMut], offset: u64) -> Result<usize> {
-        let mut fd: &File = &self.as_os_file()?.as_file();
-        let cur_pos = fd.seek(SeekFrom::Current(0))?;
-        fd.seek(SeekFrom::Start(offset))?;
-        let nread = self.read_vectored(buf)?;
-        fd.seek(SeekFrom::Start(cur_pos))?;
-        Ok(nread)
-    }
-    fn pwritev(&self, buf: &[io::IoSlice], offset: u64) -> Result<usize> {
-        let mut fd: &File = &self.as_os_file()?.as_file();
-        let cur_pos = fd.seek(SeekFrom::Current(0))?;
-        fd.seek(SeekFrom::Start(offset))?;
-        let nwritten = self.write_vectored(&buf, false)?;
-        fd.seek(SeekFrom::Start(cur_pos))?;
-        Ok(nwritten)
-    }
-    fn read_vectored(&self, iovs: &mut [io::IoSliceMut]) -> Result<usize> {
-        let nread = match self {
-            Self::OsFile(file) => file.as_file().read_vectored(iovs)?,
-            Self::Stdin => io::stdin().read_vectored(iovs)?,
-            _ => return Err(Errno::Badf),
-        };
-        Ok(nread)
-    }
-    fn readdir<'a>(
-        &'a self,
-        cookie: types::Dircookie,
-    ) -> Result<Box<dyn Iterator<Item = Result<(types::Dirent, String)>> + 'a>> {
-        fd::readdir(self.as_os_file()?, cookie)
-    }
-    fn seek(&self, offset: SeekFrom) -> Result<u64> {
-        let pos = self.as_os_file()?.as_file().seek(offset)?;
-        Ok(pos)
-    }
-    fn sync(&self) -> Result<()> {
-        self.as_os_file()?.as_file().sync_all()?;
-        Ok(())
-    }
-    fn write_vectored(&self, iovs: &[io::IoSlice], isatty: bool) -> Result<usize> {
-        let nwritten = match self {
-            Self::OsFile(file) => {
-                let mut file: &File = &file.as_file();
-                if isatty {
-                    SandboxedTTYWriter::new(&mut file).write_vectored(&iovs)?
-                } else {
-                    file.write_vectored(&iovs)?
-                }
-            }
-            Self::Stdin => return Err(Errno::Badf),
-            Self::Stdout => {
-                // lock for the duration of the scope
-                let stdout = io::stdout();
-                let mut stdout = stdout.lock();
-                let nwritten = if isatty {
-                    SandboxedTTYWriter::new(&mut stdout).write_vectored(&iovs)?
-                } else {
-                    stdout.write_vectored(&iovs)?
-                };
-                stdout.flush()?;
-                nwritten
-            }
-            // Always sanitize stderr, even if it's not directly connected to a tty,
-            // because stderr is meant for diagnostics rather than binary output,
-            // and may be redirected to a file which could end up being displayed
-            // on a tty later.
-            Self::Stderr => SandboxedTTYWriter::new(&mut io::stderr()).write_vectored(&iovs)?,
-        };
-        Ok(nwritten)
-    }
-    // PathOps
-    fn create_directory(&self, path: &str) -> Result<()> {
-        path::create_directory(self.as_os_file()?, path)
-    }
-    fn openat(
-        &self,
-        path: &str,
-        read: bool,
-        write: bool,
-        oflags: types::Oflags,
-        fd_flags: types::Fdflags,
-    ) -> Result<Box<dyn Handle>> {
-        let handle = path::open(self.as_os_file()?, path, read, write, oflags, fd_flags)?;
-        Ok(Box::new(handle))
-    }
-    fn link(
-        &self,
-        old_path: &str,
-        new_handle: Box<dyn Handle>,
-        new_path: &str,
-        follow: bool,
-    ) -> Result<()> {
-        let new_handle = match new_handle.as_any().downcast_ref::<Self>() {
-            None => {
-                error!("Tried to link OS resource with Virtual");
-                return Err(Errno::Badf);
-            }
-            Some(handle) => handle,
-        };
-        path::link(
-            self.as_os_file()?,
-            old_path,
-            new_handle.as_os_file()?,
-            new_path,
-            follow,
-        )
-    }
-    fn symlink(&self, old_path: &str, new_path: &str) -> Result<()> {
-        path::symlink(old_path, self.as_os_file()?, new_path)
-    }
-    fn readlink(&self, path: &str, buf: &mut [u8]) -> Result<usize> {
-        path::readlink(self.as_os_file()?, path, buf)
-    }
-    fn readlinkat(&self, path: &str) -> Result<String> {
-        path::readlinkat(self.as_os_file()?, path)
-    }
-    fn rename(&self, old_path: &str, new_handle: Box<dyn Handle>, new_path: &str) -> Result<()> {
-        let new_handle = match new_handle.as_any().downcast_ref::<Self>() {
-            None => {
-                error!("Tried to link OS resource with Virtual");
-                return Err(Errno::Badf);
-            }
-            Some(handle) => handle,
-        };
-        debug!("rename (old_dirfd, old_path)=({:?}, {:?})", self, old_path);
-        debug!(
-            "rename (new_dirfd, new_path)=({:?}, {:?})",
-            new_handle, new_path
-        );
-        path::rename(
-            self.as_os_file()?,
-            old_path,
-            new_handle.as_os_file()?,
-            new_path,
-        )
-    }
-    fn remove_directory(&self, path: &str) -> Result<()> {
-        debug!("remove_directory (dirfd, path)=({:?}, {:?})", self, path);
-        path::remove_directory(self.as_os_file()?, path)
-    }
-    fn unlink_file(&self, path: &str) -> Result<()> {
-        path::unlink_file(self.as_os_file()?, path)
-    }
-}
diff --git a/crates/wasi-common/src/sys/osother.rs b/crates/wasi-common/src/sys/osother.rs
new file mode 100644
index 000000000..de30a4f2d
--- /dev/null
+++ b/crates/wasi-common/src/sys/osother.rs
@@ -0,0 +1,95 @@
+use super::sys_impl::oshandle::RawOsHandle;
+use super::{fd, AsFile};
+use crate::handle::{Handle, HandleRights};
+use crate::sandboxed_tty_writer::SandboxedTTYWriter;
+use crate::wasi::types::{self, Filetype};
+use crate::wasi::Result;
+use std::any::Any;
+use std::cell::Cell;
+use std::fs::File;
+use std::io::{self, Read, Write};
+use std::ops::Deref;
+
+pub(crate) trait OsOtherExt {
+    /// Create `OsOther` as `dyn Handle` from null device.
+    fn from_null() -> io::Result<Box<dyn Handle>>;
+}
+
+/// `OsOther` is something of a catch-all for everything not covered with the specific handle
+/// types (`OsFile`, `OsDir`, `Stdio`). It currently encapsulates handles such as OS pipes,
+/// sockets, streams, etc. As such, when redirecting stdio within `WasiCtxBuilder`, the redirected
+/// pipe should be encapsulated within this instance _and not_ `OsFile` which represents a regular
+/// OS file.
+#[derive(Debug)]
+pub(crate) struct OsOther {
+    file_type: Filetype,
+    rights: Cell<HandleRights>,
+    handle: RawOsHandle,
+}
+
+impl OsOther {
+    pub(super) fn new(file_type: Filetype, rights: HandleRights, handle: RawOsHandle) -> Self {
+        let rights = Cell::new(rights);
+        Self {
+            file_type,
+            rights,
+            handle,
+        }
+    }
+}
+
+impl Deref for OsOther {
+    type Target = RawOsHandle;
+
+    fn deref(&self) -> &Self::Target {
+        &self.handle
+    }
+}
+
+impl Handle for OsOther {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
+        let file_type = self.file_type;
+        let handle = self.handle.try_clone()?;
+        let rights = self.rights.clone();
+        Ok(Box::new(Self {
+            file_type,
+            rights,
+            handle,
+        }))
+    }
+    fn get_file_type(&self) -> Filetype {
+        self.file_type
+    }
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, new_rights: HandleRights) {
+        self.rights.set(new_rights)
+    }
+    // FdOps
+    fn fdstat_get(&self) -> Result<types::Fdflags> {
+        fd::fdstat_get(&*self.as_file()?)
+    }
+    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
+        if let Some(handle) = fd::fdstat_set_flags(&*self.as_file()?, fdflags)? {
+            self.handle.update_from(handle);
+        }
+        Ok(())
+    }
+    fn read_vectored(&self, iovs: &mut [io::IoSliceMut]) -> Result<usize> {
+        let nread = self.as_file()?.read_vectored(iovs)?;
+        Ok(nread)
+    }
+    fn write_vectored(&self, iovs: &[io::IoSlice]) -> Result<usize> {
+        let mut fd: &File = &*self.as_file()?;
+        let nwritten = if self.is_tty() {
+            SandboxedTTYWriter::new(&mut fd).write_vectored(&iovs)?
+        } else {
+            fd.write_vectored(iovs)?
+        };
+        Ok(nwritten)
+    }
+}
diff --git a/crates/wasi-common/src/sys/stdio.rs b/crates/wasi-common/src/sys/stdio.rs
new file mode 100644
index 000000000..bbd2339ed
--- /dev/null
+++ b/crates/wasi-common/src/sys/stdio.rs
@@ -0,0 +1,176 @@
+// The reason we have a separate Stdio wrappers is to correctly facilitate redirects on Windows.
+// To elaborate further, in POSIX, we can get a stdio handle by opening a specific fd {0,1,2}.
+// On Windows however, we need to issue a syscall that's separate from standard Windows "open"
+// to get a console handle, and this is GetStdHandle. This is exactly what Rust does and what
+// is wrapped inside their Stdio object in the libstd. We wrap it here as well because of this
+// nuance on Windows:
+//
+//      The standard handles of a process may be redirected by a call to SetStdHandle, in which
+//      case GetStdHandle returns the redirected handle.
+//
+// The MSDN also says this however:
+//
+//      If the standard handles have been redirected, you can specify the CONIN$ value in a call
+//      to the CreateFile function to get a handle to a console's input buffer. Similarly, you
+//      can specify the CONOUT$ value to get a handle to a console's active screen buffer.
+//
+// TODO it might worth re-investigating the suitability of this type on Windows.
+
+use super::{fd, AsFile};
+use crate::handle::{Handle, HandleRights};
+use crate::sandboxed_tty_writer::SandboxedTTYWriter;
+use crate::wasi::types::{self, Filetype};
+use crate::wasi::Result;
+use std::any::Any;
+use std::cell::Cell;
+use std::io::{self, Read, Write};
+
+pub(crate) trait StdinExt: Sized {
+    /// Create `Stdin` from `io::stdin`.
+    fn stdin() -> io::Result<Box<dyn Handle>>;
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Stdin {
+    pub(crate) file_type: Filetype,
+    pub(crate) rights: Cell<HandleRights>,
+}
+
+impl Handle for Stdin {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
+        Ok(Box::new(self.clone()))
+    }
+    fn get_file_type(&self) -> Filetype {
+        self.file_type
+    }
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, new_rights: HandleRights) {
+        self.rights.set(new_rights)
+    }
+    // FdOps
+    fn fdstat_get(&self) -> Result<types::Fdflags> {
+        fd::fdstat_get(&*self.as_file()?)
+    }
+    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
+        if let Some(_) = fd::fdstat_set_flags(&*self.as_file()?, fdflags)? {
+            // OK, this means we should somehow update the underlying os handle,
+            // and we can't do that with `std::io::std{in, out, err}`, so we'll
+            // panic for now.
+            panic!("Tried updating Fdflags on Stdio handle by re-opening as file!");
+        }
+        Ok(())
+    }
+    fn read_vectored(&self, iovs: &mut [io::IoSliceMut]) -> Result<usize> {
+        let nread = io::stdin().read_vectored(iovs)?;
+        Ok(nread)
+    }
+}
+
+pub(crate) trait StdoutExt: Sized {
+    /// Create `Stdout` from `io::stdout`.
+    fn stdout() -> io::Result<Box<dyn Handle>>;
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Stdout {
+    pub(crate) file_type: Filetype,
+    pub(crate) rights: Cell<HandleRights>,
+}
+
+impl Handle for Stdout {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
+        Ok(Box::new(self.clone()))
+    }
+    fn get_file_type(&self) -> Filetype {
+        self.file_type
+    }
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, new_rights: HandleRights) {
+        self.rights.set(new_rights)
+    }
+    // FdOps
+    fn fdstat_get(&self) -> Result<types::Fdflags> {
+        fd::fdstat_get(&*self.as_file()?)
+    }
+    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
+        if let Some(_) = fd::fdstat_set_flags(&*self.as_file()?, fdflags)? {
+            // OK, this means we should somehow update the underlying os handle,
+            // and we can't do that with `std::io::std{in, out, err}`, so we'll
+            // panic for now.
+            panic!("Tried updating Fdflags on Stdio handle by re-opening as file!");
+        }
+        Ok(())
+    }
+    fn write_vectored(&self, iovs: &[io::IoSlice]) -> Result<usize> {
+        // lock for the duration of the scope
+        let stdout = io::stdout();
+        let mut stdout = stdout.lock();
+        let nwritten = if self.is_tty() {
+            SandboxedTTYWriter::new(&mut stdout).write_vectored(&iovs)?
+        } else {
+            stdout.write_vectored(iovs)?
+        };
+        stdout.flush()?;
+        Ok(nwritten)
+    }
+}
+
+pub(crate) trait StderrExt: Sized {
+    /// Create `Stderr` from `io::stderr`.
+    fn stderr() -> io::Result<Box<dyn Handle>>;
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct Stderr {
+    pub(crate) file_type: Filetype,
+    pub(crate) rights: Cell<HandleRights>,
+}
+
+impl Handle for Stderr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+    fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
+        Ok(Box::new(self.clone()))
+    }
+    fn get_file_type(&self) -> Filetype {
+        self.file_type
+    }
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, new_rights: HandleRights) {
+        self.rights.set(new_rights)
+    }
+    // FdOps
+    fn fdstat_get(&self) -> Result<types::Fdflags> {
+        fd::fdstat_get(&*self.as_file()?)
+    }
+    fn fdstat_set_flags(&self, fdflags: types::Fdflags) -> Result<()> {
+        if let Some(_) = fd::fdstat_set_flags(&*self.as_file()?, fdflags)? {
+            // OK, this means we should somehow update the underlying os handle,
+            // and we can't do that with `std::io::std{in, out, err}`, so we'll
+            // panic for now.
+            panic!("Tried updating Fdflags on Stdio handle by re-opening as file!");
+        }
+        Ok(())
+    }
+    fn write_vectored(&self, iovs: &[io::IoSlice]) -> Result<usize> {
+        // Always sanitize stderr, even if it's not directly connected to a tty,
+        // because stderr is meant for diagnostics rather than binary output,
+        // and may be redirected to a file which could end up being displayed
+        // on a tty later.
+        let nwritten = SandboxedTTYWriter::new(&mut io::stderr()).write_vectored(&iovs)?;
+        Ok(nwritten)
+    }
+}
diff --git a/crates/wasi-common/src/sys/unix/bsd/mod.rs b/crates/wasi-common/src/sys/unix/bsd/mod.rs
index a4a188245..a521318cd 100644
--- a/crates/wasi-common/src/sys/unix/bsd/mod.rs
+++ b/crates/wasi-common/src/sys/unix/bsd/mod.rs
@@ -1,4 +1,4 @@
-pub(crate) mod osfile;
+pub(crate) mod osdir;
 pub(crate) mod path;
 
 pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::SYNC;
diff --git a/crates/wasi-common/src/sys/unix/bsd/osdir.rs b/crates/wasi-common/src/sys/unix/bsd/osdir.rs
new file mode 100644
index 000000000..0726ad2ef
--- /dev/null
+++ b/crates/wasi-common/src/sys/unix/bsd/osdir.rs
@@ -0,0 +1,46 @@
+use crate::handle::HandleRights;
+use crate::sys::sys_impl::oshandle::RawOsHandle;
+use crate::wasi::Result;
+use std::cell::{Cell, RefCell, RefMut};
+use std::io;
+use yanix::dir::Dir;
+
+#[derive(Debug)]
+pub(crate) struct OsDir {
+    pub(crate) rights: Cell<HandleRights>,
+    pub(crate) handle: RawOsHandle,
+    // When the client makes a `fd_readdir` syscall on this descriptor,
+    // we will need to cache the `libc::DIR` pointer manually in order
+    // to be able to seek on it later. While on Linux, this is handled
+    // by the OS, BSD Unixes require the client to do this caching.
+    //
+    // This comes directly from the BSD man pages on `readdir`:
+    //   > Values returned by telldir() are good only for the lifetime
+    //   > of the DIR pointer, dirp, from which they are derived.
+    //   > If the directory is closed and then reopened, prior values
+    //   > returned by telldir() will no longer be valid.
+    stream_ptr: RefCell<Dir>,
+}
+
+impl OsDir {
+    pub(crate) fn new(rights: HandleRights, handle: RawOsHandle) -> io::Result<Self> {
+        let rights = Cell::new(rights);
+        // We need to duplicate the handle, because `opendir(3)`:
+        //     Upon successful return from fdopendir(), the file descriptor is under
+        //     control of the system, and if any attempt is made to close the file
+        //     descriptor, or to modify the state of the associated description other
+        //     than by means of closedir(), readdir(), readdir_r(), or rewinddir(),
+        //     the behaviour is undefined.
+        let stream_ptr = Dir::from(handle.try_clone()?)?;
+        let stream_ptr = RefCell::new(stream_ptr);
+        Ok(Self {
+            rights,
+            handle,
+            stream_ptr,
+        })
+    }
+    /// Returns the `Dir` stream pointer associated with this `OsDir`.
+    pub(crate) fn stream_ptr(&self) -> Result<RefMut<Dir>> {
+        Ok(self.stream_ptr.borrow_mut())
+    }
+}
diff --git a/crates/wasi-common/src/sys/unix/bsd/osfile.rs b/crates/wasi-common/src/sys/unix/bsd/osfile.rs
deleted file mode 100644
index 0ead55683..000000000
--- a/crates/wasi-common/src/sys/unix/bsd/osfile.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-use crate::sys::oshandle::AsFile;
-use crate::wasi::Result;
-use std::cell::{Cell, RefCell, RefMut};
-use std::fs::File;
-use std::io;
-use std::mem::ManuallyDrop;
-use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
-use yanix::dir::Dir;
-
-#[derive(Debug)]
-pub(crate) struct OsFile {
-    fd: Cell<RawFd>,
-    // In case that this `OsHandle` actually refers to a directory,
-    // when the client makes a `fd_readdir` syscall on this descriptor,
-    // we will need to cache the `libc::DIR` pointer manually in order
-    // to be able to seek on it later. While on Linux, this is handled
-    // by the OS, BSD Unixes require the client to do this caching.
-    //
-    // This comes directly from the BSD man pages on `readdir`:
-    //   > Values returned by telldir() are good only for the lifetime
-    //   > of the DIR pointer, dirp, from which they are derived.
-    //   > If the directory is closed and then reopened, prior values
-    //   > returned by telldir() will no longer be valid.
-    dir: RefCell<Option<Dir>>,
-}
-
-impl OsFile {
-    /// Consumes `other` taking the ownership of the underlying
-    /// `RawFd` file descriptor.
-    ///
-    /// Note that the state of `Dir` stream pointer *will* not be carried
-    /// across from `other` to `self`.
-    pub(crate) fn update_from(&self, other: Self) {
-        let new_fd = other.into_raw_fd();
-        let old_fd = self.fd.get();
-        self.fd.set(new_fd);
-        // We need to remember to close the old_fd.
-        unsafe {
-            File::from_raw_fd(old_fd);
-        }
-    }
-    /// Clones `self` uninitializing the `Dir` stream pointer
-    /// (if any).
-    pub(crate) fn try_clone(&self) -> io::Result<Self> {
-        let fd = self.as_file().try_clone()?;
-        Ok(Self {
-            fd: Cell::new(fd.into_raw_fd()),
-            dir: RefCell::new(None),
-        })
-    }
-    /// Returns the `Dir` stream pointer associated with
-    /// this instance.
-    ///
-    /// Initializes the `Dir` stream pointer if `None`.
-    pub(crate) fn dir_stream(&self) -> Result<RefMut<Dir>> {
-        if self.dir.borrow().is_none() {
-            // We need to duplicate the fd, because `opendir(3)`:
-            //     Upon successful return from fdopendir(), the file descriptor is under
-            //     control of the system, and if any attempt is made to close the file
-            //     descriptor, or to modify the state of the associated description other
-            //     than by means of closedir(), readdir(), readdir_r(), or rewinddir(),
-            //     the behaviour is undefined.
-            let file = self.try_clone()?;
-            let d = Dir::from(file)?;
-            *self.dir.borrow_mut() = Some(d);
-        }
-        Ok(RefMut::map(self.dir.borrow_mut(), |dir| {
-            dir.as_mut().unwrap()
-        }))
-    }
-}
-
-impl Drop for OsFile {
-    fn drop(&mut self) {
-        unsafe {
-            File::from_raw_fd(self.as_raw_fd());
-        }
-    }
-}
-
-impl AsRawFd for OsFile {
-    fn as_raw_fd(&self) -> RawFd {
-        self.fd.get()
-    }
-}
-
-impl FromRawFd for OsFile {
-    unsafe fn from_raw_fd(fd: RawFd) -> Self {
-        Self {
-            fd: Cell::new(fd),
-            dir: RefCell::new(None),
-        }
-    }
-}
-
-impl IntoRawFd for OsFile {
-    fn into_raw_fd(self) -> RawFd {
-        // We need to prevent dropping of the OsFile
-        let wrapped = ManuallyDrop::new(self);
-        wrapped.fd.get()
-    }
-}
-
-impl AsFile for OsFile {
-    fn as_file(&self) -> ManuallyDrop<File> {
-        let file = unsafe { File::from_raw_fd(self.fd.get()) };
-        ManuallyDrop::new(file)
-    }
-}
diff --git a/crates/wasi-common/src/sys/unix/bsd/path.rs b/crates/wasi-common/src/sys/unix/bsd/path.rs
index 872d3f25c..8dff951cb 100644
--- a/crates/wasi-common/src/sys/unix/bsd/path.rs
+++ b/crates/wasi-common/src/sys/unix/bsd/path.rs
@@ -1,8 +1,8 @@
-use super::osfile::OsFile;
+use crate::sys::osdir::OsDir;
 use crate::wasi::{Errno, Result};
 use std::os::unix::prelude::AsRawFd;
 
-pub(crate) fn unlink_file(dirfd: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn unlink_file(dirfd: &OsDir, path: &str) -> Result<()> {
     use yanix::file::{unlinkat, AtFlag};
     match unsafe { unlinkat(dirfd.as_raw_fd(), path, AtFlag::empty()) } {
         Err(err) => {
@@ -35,7 +35,7 @@ pub(crate) fn unlink_file(dirfd: &OsFile, path: &str) -> Result<()> {
     }
 }
 
-pub(crate) fn symlink(old_path: &str, new_dirfd: &OsFile, new_path: &str) -> Result<()> {
+pub(crate) fn symlink(old_path: &str, new_dirfd: &OsDir, new_path: &str) -> Result<()> {
     use yanix::file::{fstatat, symlinkat, AtFlag};
 
     log::debug!("path_symlink old_path = {:?}", old_path);
@@ -69,9 +69,9 @@ pub(crate) fn symlink(old_path: &str, new_dirfd: &OsFile, new_path: &str) -> Res
 }
 
 pub(crate) fn rename(
-    old_dirfd: &OsFile,
+    old_dirfd: &OsDir,
     old_path: &str,
-    new_dirfd: &OsFile,
+    new_dirfd: &OsDir,
     new_path: &str,
 ) -> Result<()> {
     use yanix::file::{fstatat, renameat, AtFlag};
diff --git a/crates/wasi-common/src/sys/unix/emscripten/mod.rs b/crates/wasi-common/src/sys/unix/emscripten/mod.rs
index 151d73502..ed0472e45 100644
--- a/crates/wasi-common/src/sys/unix/emscripten/mod.rs
+++ b/crates/wasi-common/src/sys/unix/emscripten/mod.rs
@@ -1,5 +1,5 @@
-#[path = "../linux/osfile.rs"]
-pub(crate) mod osfile;
+#[path = "../linux/osdir.rs"]
+pub(crate) mod osdir;
 #[path = "../linux/path.rs"]
 pub(crate) mod path;
 
diff --git a/crates/wasi-common/src/sys/unix/fd.rs b/crates/wasi-common/src/sys/unix/fd.rs
index e192bd44d..83d717711 100644
--- a/crates/wasi-common/src/sys/unix/fd.rs
+++ b/crates/wasi-common/src/sys/unix/fd.rs
@@ -1,4 +1,6 @@
-use super::oshandle::OsFile;
+use super::oshandle::RawOsHandle;
+use crate::sys::osdir::OsDir;
+use crate::sys::osfile::OsFile;
 use crate::wasi::{self, types, Result};
 use std::convert::TryInto;
 use std::fs::File;
@@ -9,7 +11,7 @@ pub(crate) fn fdstat_get(fd: &File) -> Result<types::Fdflags> {
     Ok(fdflags.into())
 }
 
-pub(crate) fn fdstat_set_flags(fd: &File, fdflags: types::Fdflags) -> Result<Option<OsFile>> {
+pub(crate) fn fdstat_set_flags(fd: &File, fdflags: types::Fdflags) -> Result<Option<RawOsHandle>> {
     unsafe { yanix::fcntl::set_status_flags(fd.as_raw_fd(), fdflags.into())? };
     // We return None here to signal that the operation succeeded on the original
     // file descriptor and mutating the original WASI Descriptor is thus unnecessary.
@@ -45,14 +47,14 @@ pub(crate) fn filestat_get(file: &File) -> Result<types::Filestat> {
 }
 
 pub(crate) fn readdir<'a>(
-    file: &'a OsFile,
+    dirfd: &'a OsDir,
     cookie: types::Dircookie,
 ) -> Result<Box<dyn Iterator<Item = Result<(types::Dirent, String)>> + 'a>> {
     use yanix::dir::{DirIter, Entry, EntryExt, SeekLoc};
 
     // Get an instance of `Dir`; this is host-specific due to intricasies
     // of managing a dir stream between Linux and BSD *nixes
-    let mut dir = file.dir_stream()?;
+    let mut dir = dirfd.stream_ptr()?;
 
     // Seek if needed. Unless cookie is wasi::__WASI_DIRCOOKIE_START,
     // new items may not be returned to the caller.
diff --git a/crates/wasi-common/src/sys/unix/linux/mod.rs b/crates/wasi-common/src/sys/unix/linux/mod.rs
index b587ed6b6..d8bd3e750 100644
--- a/crates/wasi-common/src/sys/unix/linux/mod.rs
+++ b/crates/wasi-common/src/sys/unix/linux/mod.rs
@@ -1,4 +1,4 @@
-pub(crate) mod osfile;
+pub(crate) mod osdir;
 pub(crate) mod path;
 
 pub(crate) const O_RSYNC: yanix::file::OFlag = yanix::file::OFlag::RSYNC;
diff --git a/crates/wasi-common/src/sys/unix/linux/osdir.rs b/crates/wasi-common/src/sys/unix/linux/osdir.rs
new file mode 100644
index 000000000..2fff99ec7
--- /dev/null
+++ b/crates/wasi-common/src/sys/unix/linux/osdir.rs
@@ -0,0 +1,35 @@
+use crate::handle::HandleRights;
+use crate::sys::sys_impl::oshandle::RawOsHandle;
+use crate::wasi::Result;
+use std::cell::Cell;
+use std::io;
+use yanix::dir::Dir;
+
+#[derive(Debug)]
+pub(crate) struct OsDir {
+    pub(crate) rights: Cell<HandleRights>,
+    pub(crate) handle: RawOsHandle,
+}
+
+impl OsDir {
+    pub(crate) fn new(rights: HandleRights, handle: RawOsHandle) -> io::Result<Self> {
+        let rights = Cell::new(rights);
+        Ok(Self { rights, handle })
+    }
+    /// Returns the `Dir` stream pointer associated with this `OsDir`.
+    pub(crate) fn stream_ptr(&self) -> Result<Box<Dir>> {
+        // We need to duplicate the handle, because `opendir(3)`:
+        //     After a successful call to fdopendir(), fd is used internally by the implementation,
+        //     and should not otherwise be used by the application.
+        // `opendir(3p)` also says that it's undefined behavior to
+        // modify the state of the fd in a different way than by accessing DIR*.
+        //
+        // Still, rewinddir will be needed because the two file descriptors
+        // share progress. But we can safely execute closedir now.
+        let file = self.handle.try_clone()?;
+        // TODO This doesn't look very clean. Can we do something about it?
+        // Boxing is needed here in order to satisfy `yanix`'s trait requirement for the `DirIter`
+        // where `T: Deref<Target = Dir>`.
+        Ok(Box::new(Dir::from(file)?))
+    }
+}
diff --git a/crates/wasi-common/src/sys/unix/linux/osfile.rs b/crates/wasi-common/src/sys/unix/linux/osfile.rs
deleted file mode 100644
index 388f245df..000000000
--- a/crates/wasi-common/src/sys/unix/linux/osfile.rs
+++ /dev/null
@@ -1,82 +0,0 @@
-use crate::sys::oshandle::AsFile;
-use crate::wasi::Result;
-use std::cell::Cell;
-use std::fs::File;
-use std::io;
-use std::mem::ManuallyDrop;
-use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
-use yanix::dir::Dir;
-
-#[derive(Debug)]
-pub(crate) struct OsFile(Cell<RawFd>);
-
-impl OsFile {
-    /// Consumes `other` taking the ownership of the underlying
-    /// `RawFd` file descriptor.
-    pub(crate) fn update_from(&self, other: Self) {
-        let new_fd = other.into_raw_fd();
-        let old_fd = self.0.get();
-        self.0.set(new_fd);
-        // We need to remember to close the old_fd.
-        unsafe {
-            File::from_raw_fd(old_fd);
-        }
-    }
-    /// Clones `self`.
-    pub(crate) fn try_clone(&self) -> io::Result<Self> {
-        let fd = self.as_file().try_clone()?;
-        Ok(Self(Cell::new(fd.into_raw_fd())))
-    }
-    /// Returns the `Dir` stream pointer associated with
-    /// this instance.
-    pub(crate) fn dir_stream(&self) -> Result<Box<Dir>> {
-        // We need to duplicate the fd, because `opendir(3)`:
-        //     After a successful call to fdopendir(), fd is used internally by the implementation,
-        //     and should not otherwise be used by the application.
-        // `opendir(3p)` also says that it's undefined behavior to
-        // modify the state of the fd in a different way than by accessing DIR*.
-        //
-        // Still, rewinddir will be needed because the two file descriptors
-        // share progress. But we can safely execute closedir now.
-        let file = self.try_clone()?;
-        // TODO This doesn't look very clean. Can we do something about it?
-        // Boxing is needed here in order to satisfy `yanix`'s trait requirement for the `DirIter`
-        // where `T: Deref<Target = Dir>`.
-        Ok(Box::new(Dir::from(file)?))
-    }
-}
-
-impl Drop for OsFile {
-    fn drop(&mut self) {
-        unsafe {
-            File::from_raw_fd(self.as_raw_fd());
-        }
-    }
-}
-
-impl AsRawFd for OsFile {
-    fn as_raw_fd(&self) -> RawFd {
-        self.0.get()
-    }
-}
-
-impl FromRawFd for OsFile {
-    unsafe fn from_raw_fd(fd: RawFd) -> Self {
-        Self(Cell::new(fd))
-    }
-}
-
-impl IntoRawFd for OsFile {
-    fn into_raw_fd(self) -> RawFd {
-        // We need to prevent dropping of the OsFile
-        let wrapped = ManuallyDrop::new(self);
-        wrapped.0.get()
-    }
-}
-
-impl AsFile for OsFile {
-    fn as_file(&self) -> ManuallyDrop<File> {
-        let file = unsafe { File::from_raw_fd(self.0.get()) };
-        ManuallyDrop::new(file)
-    }
-}
diff --git a/crates/wasi-common/src/sys/unix/linux/path.rs b/crates/wasi-common/src/sys/unix/linux/path.rs
index 02ee2b3fd..611c57d22 100644
--- a/crates/wasi-common/src/sys/unix/linux/path.rs
+++ b/crates/wasi-common/src/sys/unix/linux/path.rs
@@ -1,14 +1,14 @@
-use super::osfile::OsFile;
+use crate::sys::osdir::OsDir;
 use crate::wasi::Result;
 use std::os::unix::prelude::AsRawFd;
 
-pub(crate) fn unlink_file(dirfd: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn unlink_file(dirfd: &OsDir, path: &str) -> Result<()> {
     use yanix::file::{unlinkat, AtFlag};
     unsafe { unlinkat(dirfd.as_raw_fd(), path, AtFlag::empty())? };
     Ok(())
 }
 
-pub(crate) fn symlink(old_path: &str, new_dirfd: &OsFile, new_path: &str) -> Result<()> {
+pub(crate) fn symlink(old_path: &str, new_dirfd: &OsDir, new_path: &str) -> Result<()> {
     use yanix::file::symlinkat;
 
     log::debug!("path_symlink old_path = {:?}", old_path);
@@ -23,9 +23,9 @@ pub(crate) fn symlink(old_path: &str, new_dirfd: &OsFile, new_path: &str) -> Res
 }
 
 pub(crate) fn rename(
-    old_dirfd: &OsFile,
+    old_dirfd: &OsDir,
     old_path: &str,
-    new_dirfd: &OsFile,
+    new_dirfd: &OsDir,
     new_path: &str,
 ) -> Result<()> {
     use yanix::file::renameat;
diff --git a/crates/wasi-common/src/sys/unix/mod.rs b/crates/wasi-common/src/sys/unix/mod.rs
index 8bad50fc7..e71635db0 100644
--- a/crates/wasi-common/src/sys/unix/mod.rs
+++ b/crates/wasi-common/src/sys/unix/mod.rs
@@ -1,8 +1,12 @@
 pub(crate) mod clock;
 pub(crate) mod fd;
+pub(crate) mod osdir;
+pub(crate) mod osfile;
 pub(crate) mod oshandle;
+pub(crate) mod osother;
 pub(crate) mod path;
 pub(crate) mod poll;
+pub(crate) mod stdio;
 
 cfg_if::cfg_if! {
     if #[cfg(target_os = "linux")] {
@@ -22,16 +26,105 @@ cfg_if::cfg_if! {
     }
 }
 
-use crate::wasi::{types, Errno, Result};
+use crate::handle::HandleRights;
+use crate::sys::AsFile;
+use crate::wasi::{types, Errno, Result, RightsExt};
 use std::convert::{TryFrom, TryInto};
 use std::fs::File;
 use std::io;
+use std::mem::ManuallyDrop;
+use std::os::unix::prelude::{AsRawFd, FileTypeExt, FromRawFd};
 use std::path::Path;
 use yanix::clock::ClockId;
 use yanix::file::{AtFlag, OFlag};
 
 pub(crate) use sys_impl::*;
 
+impl<T: AsRawFd> AsFile for T {
+    fn as_file(&self) -> io::Result<ManuallyDrop<File>> {
+        let file = unsafe { File::from_raw_fd(self.as_raw_fd()) };
+        Ok(ManuallyDrop::new(file))
+    }
+}
+
+pub(super) fn get_file_type(file: &File) -> io::Result<types::Filetype> {
+    let ft = file.metadata()?.file_type();
+    let file_type = if ft.is_block_device() {
+        log::debug!("Host fd {:?} is a block device", file.as_raw_fd());
+        types::Filetype::BlockDevice
+    } else if ft.is_char_device() {
+        log::debug!("Host fd {:?} is a char device", file.as_raw_fd());
+        types::Filetype::CharacterDevice
+    } else if ft.is_dir() {
+        log::debug!("Host fd {:?} is a directory", file.as_raw_fd());
+        types::Filetype::Directory
+    } else if ft.is_file() {
+        log::debug!("Host fd {:?} is a file", file.as_raw_fd());
+        types::Filetype::RegularFile
+    } else if ft.is_socket() {
+        log::debug!("Host fd {:?} is a socket", file.as_raw_fd());
+        use yanix::socket::{get_socket_type, SockType};
+        match unsafe { get_socket_type(file.as_raw_fd())? } {
+            SockType::Datagram => types::Filetype::SocketDgram,
+            SockType::Stream => types::Filetype::SocketStream,
+            _ => return Err(io::Error::from_raw_os_error(libc::EINVAL)),
+        }
+    } else if ft.is_fifo() {
+        log::debug!("Host fd {:?} is a fifo", file.as_raw_fd());
+        types::Filetype::Unknown
+    } else {
+        log::debug!("Host fd {:?} is unknown", file.as_raw_fd());
+        return Err(io::Error::from_raw_os_error(libc::EINVAL));
+    };
+    Ok(file_type)
+}
+
+pub(super) fn get_rights(file: &File, file_type: &types::Filetype) -> io::Result<HandleRights> {
+    use yanix::{fcntl, file::OFlag};
+    let (base, inheriting) = match file_type {
+        types::Filetype::BlockDevice => (
+            types::Rights::block_device_base(),
+            types::Rights::block_device_inheriting(),
+        ),
+        types::Filetype::CharacterDevice => {
+            use yanix::file::isatty;
+            if unsafe { isatty(file.as_raw_fd())? } {
+                (types::Rights::tty_base(), types::Rights::tty_base())
+            } else {
+                (
+                    types::Rights::character_device_base(),
+                    types::Rights::character_device_inheriting(),
+                )
+            }
+        }
+        types::Filetype::SocketDgram | types::Filetype::SocketStream => (
+            types::Rights::socket_base(),
+            types::Rights::socket_inheriting(),
+        ),
+        types::Filetype::SymbolicLink | types::Filetype::Unknown => (
+            types::Rights::regular_file_base(),
+            types::Rights::regular_file_inheriting(),
+        ),
+        types::Filetype::Directory => (
+            types::Rights::directory_base(),
+            types::Rights::directory_inheriting(),
+        ),
+        types::Filetype::RegularFile => (
+            types::Rights::regular_file_base(),
+            types::Rights::regular_file_inheriting(),
+        ),
+    };
+    let mut rights = HandleRights::new(base, inheriting);
+    let flags = unsafe { fcntl::get_status_flags(file.as_raw_fd())? };
+    let accmode = flags & OFlag::ACCMODE;
+    if accmode == OFlag::RDONLY {
+        rights.base &= !types::Rights::FD_WRITE;
+    } else if accmode == OFlag::WRONLY {
+        rights.base &= !types::Rights::FD_READ;
+    }
+    Ok(rights)
+}
+
 pub fn preopen_dir<P: AsRef<Path>>(path: P) -> io::Result<File> {
     File::open(path)
 }
diff --git a/crates/wasi-common/src/sys/unix/osdir.rs b/crates/wasi-common/src/sys/unix/osdir.rs
new file mode 100644
index 000000000..47b264208
--- /dev/null
+++ b/crates/wasi-common/src/sys/unix/osdir.rs
@@ -0,0 +1,39 @@
+use super::oshandle::RawOsHandle;
+use crate::handle::HandleRights;
+use crate::wasi::{types, RightsExt};
+use std::convert::TryFrom;
+use std::fs::File;
+use std::io;
+use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd};
+
+pub(crate) use super::sys_impl::osdir::OsDir;
+
+impl TryFrom<File> for OsDir {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let ft = file.metadata()?.file_type();
+        if !ft.is_dir() {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+        let rights = get_rights(&file)?;
+        let handle = unsafe { RawOsHandle::from_raw_fd(file.into_raw_fd()) };
+        Self::new(rights, handle)
+    }
+}
+
+fn get_rights(file: &File) -> io::Result<HandleRights> {
+    use yanix::{fcntl, file::OFlag};
+    let mut rights = HandleRights::new(
+        types::Rights::directory_base(),
+        types::Rights::directory_inheriting(),
+    );
+    let flags = unsafe { fcntl::get_status_flags(file.as_raw_fd())? };
+    let accmode = flags & OFlag::ACCMODE;
+    if accmode == OFlag::RDONLY {
+        rights.base &= !types::Rights::FD_WRITE;
+    } else if accmode == OFlag::WRONLY {
+        rights.base &= !types::Rights::FD_READ;
+    }
+    Ok(rights)
+}
diff --git a/crates/wasi-common/src/sys/unix/osfile.rs b/crates/wasi-common/src/sys/unix/osfile.rs
new file mode 100644
index 000000000..f4d3b6e85
--- /dev/null
+++ b/crates/wasi-common/src/sys/unix/osfile.rs
@@ -0,0 +1,38 @@
+use super::oshandle::RawOsHandle;
+use crate::handle::HandleRights;
+use crate::sys::osfile::OsFile;
+use crate::wasi::{types, RightsExt};
+use std::convert::TryFrom;
+use std::fs::File;
+use std::io;
+use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd};
+
+impl TryFrom<File> for OsFile {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let ft = file.metadata()?.file_type();
+        if !ft.is_file() {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+        let rights = get_rights(&file)?;
+        let handle = unsafe { RawOsHandle::from_raw_fd(file.into_raw_fd()) };
+        Ok(Self::new(rights, handle))
+    }
+}
+
+fn get_rights(file: &File) -> io::Result<HandleRights> {
+    use yanix::{fcntl, file::OFlag};
+    let mut rights = HandleRights::new(
+        types::Rights::regular_file_base(),
+        types::Rights::regular_file_inheriting(),
+    );
+    let flags = unsafe { fcntl::get_status_flags(file.as_raw_fd())? };
+    let accmode = flags & OFlag::ACCMODE;
+    if accmode == OFlag::RDONLY {
+        rights.base &= !types::Rights::FD_WRITE;
+    } else if accmode == OFlag::WRONLY {
+        rights.base &= !types::Rights::FD_READ;
+    }
+    Ok(rights)
+}
diff --git a/crates/wasi-common/src/sys/unix/oshandle.rs b/crates/wasi-common/src/sys/unix/oshandle.rs
index 236ba0788..513e4b778 100644
--- a/crates/wasi-common/src/sys/unix/oshandle.rs
+++ b/crates/wasi-common/src/sys/unix/oshandle.rs
@@ -1,123 +1,40 @@
-use crate::entry::EntryRights;
-use crate::sys::oshandle::{AsFile, OsHandle, OsHandleExt};
-use crate::wasi::{types, RightsExt};
-use std::fs::{File, OpenOptions};
+use std::fs::File;
 use std::io;
-use std::mem::ManuallyDrop;
-use std::os::unix::prelude::{AsRawFd, FileTypeExt, FromRawFd, IntoRawFd, RawFd};
+use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
 
-pub(crate) use super::sys_impl::osfile::*;
+#[derive(Debug)]
+pub(crate) struct RawOsHandle(File);
 
-impl AsRawFd for OsHandle {
-    fn as_raw_fd(&self) -> RawFd {
-        match self {
-            Self::OsFile(file) => file.as_raw_fd(),
-            Self::Stdin => io::stdin().as_raw_fd(),
-            Self::Stdout => io::stdout().as_raw_fd(),
-            Self::Stderr => io::stderr().as_raw_fd(),
-        }
+impl RawOsHandle {
+    /// Tries clone `self`.
+    pub(crate) fn try_clone(&self) -> io::Result<Self> {
+        let fd = self.0.try_clone()?;
+        Ok(unsafe { Self::from_raw_fd(fd.into_raw_fd()) })
     }
-}
-
-impl AsFile for OsHandle {
-    fn as_file(&self) -> ManuallyDrop<File> {
-        let file = unsafe { File::from_raw_fd(self.as_raw_fd()) };
-        ManuallyDrop::new(file)
+    /// Consumes `other` taking the ownership of the underlying
+    /// `RawFd` file descriptor.
+    ///
+    /// Note that the state of `Dir` stream pointer *will* not be carried
+    /// across from `other` to `self`.
+    pub(crate) fn update_from(&self, _other: Self) {
+        panic!("RawOsHandle::update_from should never be issued on Unix!")
     }
 }
 
-impl From<File> for OsHandle {
-    fn from(file: File) -> Self {
-        Self::from(unsafe { OsFile::from_raw_fd(file.into_raw_fd()) })
+impl AsRawFd for RawOsHandle {
+    fn as_raw_fd(&self) -> RawFd {
+        self.0.as_raw_fd()
     }
 }
 
-impl OsHandleExt for OsHandle {
-    fn get_file_type(&self) -> io::Result<types::Filetype> {
-        let file = self.as_file();
-        let ft = file.metadata()?.file_type();
-        let file_type = if ft.is_block_device() {
-            log::debug!("Host fd {:?} is a block device", self.as_raw_fd());
-            types::Filetype::BlockDevice
-        } else if ft.is_char_device() {
-            log::debug!("Host fd {:?} is a char device", self.as_raw_fd());
-            types::Filetype::CharacterDevice
-        } else if ft.is_dir() {
-            log::debug!("Host fd {:?} is a directory", self.as_raw_fd());
-            types::Filetype::Directory
-        } else if ft.is_file() {
-            log::debug!("Host fd {:?} is a file", self.as_raw_fd());
-            types::Filetype::RegularFile
-        } else if ft.is_socket() {
-            log::debug!("Host fd {:?} is a socket", self.as_raw_fd());
-            use yanix::socket::{get_socket_type, SockType};
-            match unsafe { get_socket_type(self.as_raw_fd())? } {
-                SockType::Datagram => types::Filetype::SocketDgram,
-                SockType::Stream => types::Filetype::SocketStream,
-                _ => return Err(io::Error::from_raw_os_error(libc::EINVAL)),
-            }
-        } else if ft.is_fifo() {
-            log::debug!("Host fd {:?} is a fifo", self.as_raw_fd());
-            types::Filetype::Unknown
-        } else {
-            log::debug!("Host fd {:?} is unknown", self.as_raw_fd());
-            return Err(io::Error::from_raw_os_error(libc::EINVAL));
-        };
-
-        Ok(file_type)
-    }
-
-    fn get_rights(&self, file_type: types::Filetype) -> io::Result<EntryRights> {
-        use yanix::{fcntl, file::OFlag};
-        let (base, inheriting) = match file_type {
-            types::Filetype::BlockDevice => (
-                types::Rights::block_device_base(),
-                types::Rights::block_device_inheriting(),
-            ),
-            types::Filetype::CharacterDevice => {
-                use yanix::file::isatty;
-                if unsafe { isatty(self.as_raw_fd())? } {
-                    (types::Rights::tty_base(), types::Rights::tty_base())
-                } else {
-                    (
-                        types::Rights::character_device_base(),
-                        types::Rights::character_device_inheriting(),
-                    )
-                }
-            }
-            types::Filetype::Directory => (
-                types::Rights::directory_base(),
-                types::Rights::directory_inheriting(),
-            ),
-            types::Filetype::RegularFile => (
-                types::Rights::regular_file_base(),
-                types::Rights::regular_file_inheriting(),
-            ),
-            types::Filetype::SocketDgram | types::Filetype::SocketStream => (
-                types::Rights::socket_base(),
-                types::Rights::socket_inheriting(),
-            ),
-            types::Filetype::SymbolicLink | types::Filetype::Unknown => (
-                types::Rights::regular_file_base(),
-                types::Rights::regular_file_inheriting(),
-            ),
-        };
-        let mut rights = EntryRights::new(base, inheriting);
-        let flags = unsafe { fcntl::get_status_flags(self.as_raw_fd())? };
-        let accmode = flags & OFlag::ACCMODE;
-        if accmode == OFlag::RDONLY {
-            rights.base &= !types::Rights::FD_WRITE;
-        } else if accmode == OFlag::WRONLY {
-            rights.base &= !types::Rights::FD_READ;
-        }
-        Ok(rights)
+impl IntoRawFd for RawOsHandle {
+    fn into_raw_fd(self) -> RawFd {
+        self.0.into_raw_fd()
     }
+}
 
-    fn from_null() -> io::Result<Self> {
-        let file = OpenOptions::new()
-            .read(true)
-            .write(true)
-            .open("/dev/null")?;
-        Ok(Self::from(file))
+impl FromRawFd for RawOsHandle {
+    unsafe fn from_raw_fd(raw: RawFd) -> Self {
+        Self(File::from_raw_fd(raw))
     }
 }
diff --git a/crates/wasi-common/src/sys/unix/osother.rs b/crates/wasi-common/src/sys/unix/osother.rs
new file mode 100644
index 000000000..260c47db8
--- /dev/null
+++ b/crates/wasi-common/src/sys/unix/osother.rs
@@ -0,0 +1,34 @@
+use super::oshandle::RawOsHandle;
+use super::{get_file_type, get_rights};
+use crate::handle::Handle;
+use crate::sys::osother::{OsOther, OsOtherExt};
+use crate::wasi::types;
+use std::convert::TryFrom;
+use std::fs::{File, OpenOptions};
+use std::io;
+use std::os::unix::prelude::{FromRawFd, IntoRawFd};
+
+impl TryFrom<File> for OsOther {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let file_type = get_file_type(&file)?;
+        if file_type == types::Filetype::RegularFile || file_type == types::Filetype::Directory {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+        let rights = get_rights(&file, &file_type)?;
+        let handle = unsafe { RawOsHandle::from_raw_fd(file.into_raw_fd()) };
+        Ok(Self::new(file_type, rights, handle))
+    }
+}
+
+impl OsOtherExt for OsOther {
+    fn from_null() -> io::Result<Box<dyn Handle>> {
+        let file = OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open("/dev/null")?;
+        let file = Self::try_from(file)?;
+        Ok(Box::new(file))
+    }
+}
diff --git a/crates/wasi-common/src/sys/unix/path.rs b/crates/wasi-common/src/sys/unix/path.rs
index e52aa0f67..9ffa5f7b6 100644
--- a/crates/wasi-common/src/sys/unix/path.rs
+++ b/crates/wasi-common/src/sys/unix/path.rs
@@ -1,8 +1,9 @@
-use super::oshandle::OsFile;
-use crate::entry::EntryRights;
-use crate::sys::oshandle::OsHandle;
+use crate::handle::{Handle, HandleRights};
+use crate::sys::osdir::OsDir;
 use crate::wasi::{types, Errno, Result};
+use std::convert::TryFrom;
 use std::ffi::OsStr;
+use std::fs::File;
 use std::os::unix::prelude::{AsRawFd, FromRawFd, OsStrExt};
 use std::str;
 use yanix::file::OFlag;
@@ -19,10 +20,10 @@ pub(crate) fn from_host<S: AsRef<OsStr>>(s: S) -> Result<String> {
 }
 
 pub(crate) fn open_rights(
-    input_rights: &EntryRights,
+    input_rights: &HandleRights,
     oflags: types::Oflags,
     fs_flags: types::Fdflags,
-) -> EntryRights {
+) -> HandleRights {
     // which rights are needed on the dirfd?
     let mut needed_base = types::Rights::PATH_OPEN;
     let mut needed_inheriting = input_rights.base | input_rights.inheriting;
@@ -45,10 +46,10 @@ pub(crate) fn open_rights(
         needed_inheriting |= types::Rights::FD_SYNC;
     }
 
-    EntryRights::new(needed_base, needed_inheriting)
+    HandleRights::new(needed_base, needed_inheriting)
 }
 
-pub(crate) fn readlinkat(dirfd: &OsFile, path: &str) -> Result<String> {
+pub(crate) fn readlinkat(dirfd: &OsDir, path: &str) -> Result<String> {
     use std::os::unix::prelude::AsRawFd;
     use yanix::file::readlinkat;
 
@@ -59,16 +60,16 @@ pub(crate) fn readlinkat(dirfd: &OsFile, path: &str) -> Result<String> {
     Ok(path)
 }
 
-pub(crate) fn create_directory(base: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn create_directory(base: &OsDir, path: &str) -> Result<()> {
     use yanix::file::{mkdirat, Mode};
     unsafe { mkdirat(base.as_raw_fd(), path, Mode::from_bits_truncate(0o777))? };
     Ok(())
 }
 
 pub(crate) fn link(
-    old_dirfd: &OsFile,
+    old_dirfd: &OsDir,
     old_path: &str,
-    new_dirfd: &OsFile,
+    new_dirfd: &OsDir,
     new_path: &str,
     follow_symlinks: bool,
 ) -> Result<()> {
@@ -91,13 +92,13 @@ pub(crate) fn link(
 }
 
 pub(crate) fn open(
-    dirfd: &OsFile,
+    dirfd: &OsDir,
     path: &str,
     read: bool,
     write: bool,
     oflags: types::Oflags,
     fs_flags: types::Fdflags,
-) -> Result<OsHandle> {
+) -> Result<Box<dyn Handle>> {
     use yanix::file::{fstatat, openat, AtFlag, FileType, Mode, OFlag};
 
     let mut nix_all_oflags = if read && write {
@@ -181,10 +182,12 @@ pub(crate) fn open(
     log::debug!("path_open (host) new_fd = {:?}", new_fd);
 
     // Determine the type of the new file descriptor and which rights contradict with this type
-    Ok(OsHandle::from(unsafe { OsFile::from_raw_fd(new_fd) }))
+    let file = unsafe { File::from_raw_fd(new_fd) };
+    let handle = <Box<dyn Handle>>::try_from(file)?;
+    Ok(handle)
 }
 
-pub(crate) fn readlink(dirfd: &OsFile, path: &str, buf: &mut [u8]) -> Result<usize> {
+pub(crate) fn readlink(dirfd: &OsDir, path: &str, buf: &mut [u8]) -> Result<usize> {
     use std::cmp::min;
     use yanix::file::readlinkat;
     let read_link = unsafe { readlinkat(dirfd.as_raw_fd(), path)? };
@@ -196,7 +199,7 @@ pub(crate) fn readlink(dirfd: &OsFile, path: &str, buf: &mut [u8]) -> Result<usi
     Ok(copy_len)
 }
 
-pub(crate) fn remove_directory(dirfd: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn remove_directory(dirfd: &OsDir, path: &str) -> Result<()> {
     use yanix::file::{unlinkat, AtFlag};
     unsafe { unlinkat(dirfd.as_raw_fd(), path, AtFlag::REMOVEDIR)? };
     Ok(())
diff --git a/crates/wasi-common/src/sys/unix/poll.rs b/crates/wasi-common/src/sys/unix/poll.rs
index 7b5ffddde..b185bdb93 100644
--- a/crates/wasi-common/src/sys/unix/poll.rs
+++ b/crates/wasi-common/src/sys/unix/poll.rs
@@ -1,6 +1,6 @@
-use super::super::oshandle::OsHandle;
+use crate::entry::EntryHandle;
 use crate::poll::{ClockEventData, FdEventData};
-use crate::sys::oshandle::AsFile;
+use crate::sys::AsFile;
 use crate::wasi::{types, Errno, Result};
 use std::io;
 use std::{convert::TryInto, os::unix::prelude::AsRawFd};
@@ -16,7 +16,7 @@ pub(crate) fn oneoff(
         return Ok(());
     }
 
-    let mut poll_fds: Vec<_> = fd_events
+    let poll_fds: Result<Vec<_>> = fd_events
         .iter()
         .map(|event| {
             let mut flags = PollFlags::empty();
@@ -28,14 +28,11 @@ pub(crate) fn oneoff(
                 // events we filtered before. If we get something else here, the code has a serious bug.
                 _ => unreachable!(),
             };
-            let handle = event
-                .handle
-                .as_any()
-                .downcast_ref::<OsHandle>()
-                .expect("can poll FdEvent for OS resources only");
-            unsafe { PollFd::new(handle.as_raw_fd(), flags) }
+            let file = event.handle.as_file()?;
+            unsafe { Ok(PollFd::new(file.as_raw_fd(), flags)) }
         })
         .collect();
+    let mut poll_fds = poll_fds?;
 
     let poll_timeout = timeout.map_or(-1, |timeout| {
         let delay = timeout.delay / 1_000_000; // poll syscall requires delay to expressed in milliseconds
@@ -80,18 +77,17 @@ fn handle_fd_event(
     ready_events: impl Iterator<Item = (FdEventData, yanix::poll::PollFd)>,
     events: &mut Vec<types::Event>,
 ) -> Result<()> {
-    fn query_nbytes(handle: &OsHandle) -> Result<u64> {
-        // fionread may overflow for large files, so use another way for regular files.
-        if let OsHandle::OsFile(file) = handle {
-            let meta = file.as_file().metadata()?;
-            if meta.file_type().is_file() {
-                use yanix::file::tell;
-                let len = meta.len();
-                let host_offset = unsafe { tell(file.as_raw_fd())? };
-                return Ok(len - host_offset);
-            }
+    fn query_nbytes(handle: EntryHandle) -> Result<u64> {
+        let file = handle.as_file()?;
+        if handle.get_file_type() == types::Filetype::RegularFile {
+            // fionread may overflow for large files, so use another way for regular files.
+            use yanix::file::tell;
+            let meta = file.metadata()?;
+            let len = meta.len();
+            let host_offset = unsafe { tell(file.as_raw_fd())? };
+            return Ok(len - host_offset);
         }
-        unsafe { Ok(fionread(handle.as_raw_fd())?.into()) }
+        Ok(unsafe { fionread(file.as_raw_fd())?.into() })
     }
 
     for (fd_event, poll_fd) in ready_events {
@@ -106,12 +102,7 @@ fn handle_fd_event(
         log::debug!("poll_oneoff_handle_fd_event revents = {:?}", revents);
 
         let nbytes = if fd_event.r#type == types::Eventtype::FdRead {
-            let handle = fd_event
-                .handle
-                .as_any()
-                .downcast_ref::<OsHandle>()
-                .expect("can poll FdEvent for OS resources only");
-            query_nbytes(handle)?
+            query_nbytes(fd_event.handle)?
         } else {
             0
         };
diff --git a/crates/wasi-common/src/sys/unix/stdio.rs b/crates/wasi-common/src/sys/unix/stdio.rs
new file mode 100644
index 000000000..5a38a2992
--- /dev/null
+++ b/crates/wasi-common/src/sys/unix/stdio.rs
@@ -0,0 +1,59 @@
+use super::{get_file_type, get_rights};
+use crate::handle::Handle;
+use crate::sys::stdio::{Stderr, StderrExt, Stdin, StdinExt, Stdout, StdoutExt};
+use std::cell::Cell;
+use std::fs::File;
+use std::io;
+use std::mem::ManuallyDrop;
+use std::os::unix::prelude::{AsRawFd, FromRawFd, RawFd};
+
+impl AsRawFd for Stdin {
+    fn as_raw_fd(&self) -> RawFd {
+        io::stdin().as_raw_fd()
+    }
+}
+
+impl AsRawFd for Stdout {
+    fn as_raw_fd(&self) -> RawFd {
+        io::stdout().as_raw_fd()
+    }
+}
+
+impl AsRawFd for Stderr {
+    fn as_raw_fd(&self) -> RawFd {
+        io::stderr().as_raw_fd()
+    }
+}
+
+impl StdinExt for Stdin {
+    fn stdin() -> io::Result<Box<dyn Handle>> {
+        let file = unsafe { File::from_raw_fd(io::stdin().as_raw_fd()) };
+        let file = ManuallyDrop::new(file);
+        let file_type = get_file_type(&file)?;
+        let rights = get_rights(&file, &file_type)?;
+        let rights = Cell::new(rights);
+        Ok(Box::new(Self { file_type, rights }))
+    }
+}
+
+impl StdoutExt for Stdout {
+    fn stdout() -> io::Result<Box<dyn Handle>> {
+        let file = unsafe { File::from_raw_fd(io::stdout().as_raw_fd()) };
+        let file = ManuallyDrop::new(file);
+        let file_type = get_file_type(&file)?;
+        let rights = get_rights(&file, &file_type)?;
+        let rights = Cell::new(rights);
+        Ok(Box::new(Self { file_type, rights }))
+    }
+}
+
+impl StderrExt for Stderr {
+    fn stderr() -> io::Result<Box<dyn Handle>> {
+        let file = unsafe { File::from_raw_fd(io::stderr().as_raw_fd()) };
+        let file = ManuallyDrop::new(file);
+        let file_type = get_file_type(&file)?;
+        let rights = get_rights(&file, &file_type)?;
+        let rights = Cell::new(rights);
+        Ok(Box::new(Self { file_type, rights }))
+    }
+}
diff --git a/crates/wasi-common/src/sys/windows/fd.rs b/crates/wasi-common/src/sys/windows/fd.rs
index 0cc20a79b..78604a471 100644
--- a/crates/wasi-common/src/sys/windows/fd.rs
+++ b/crates/wasi-common/src/sys/windows/fd.rs
@@ -1,7 +1,9 @@
 use super::file_serial_no;
-use super::oshandle::OsFile;
+use super::oshandle::RawOsHandle;
 use crate::path;
-use crate::sys::oshandle::AsFile;
+use crate::sys::osdir::OsDir;
+use crate::sys::osfile::OsFile;
+use crate::sys::AsFile;
 use crate::wasi::{types, Result};
 use log::trace;
 use std::convert::TryInto;
@@ -39,7 +41,10 @@ pub(crate) fn fdstat_get(file: &File) -> Result<types::Fdflags> {
 // handle came from `CreateFile`, but the Rust's libstd will use `GetStdHandle`
 // rather than `CreateFile`. Relevant discussion can be found in:
 // https://github.com/rust-lang/rust/issues/40490
-pub(crate) fn fdstat_set_flags(file: &File, fdflags: types::Fdflags) -> Result<Option<OsFile>> {
+pub(crate) fn fdstat_set_flags(
+    file: &File,
+    fdflags: types::Fdflags,
+) -> Result<Option<RawOsHandle>> {
     let handle = file.as_raw_handle();
     let access_mode = winx::file::query_access_information(handle)?;
     let new_access_mode = file_access_mode_from_fdflags(
@@ -49,7 +54,7 @@ pub(crate) fn fdstat_set_flags(file: &File, fdflags: types::Fdflags) -> Result<O
             | access_mode.contains(AccessMode::FILE_APPEND_DATA),
     );
     unsafe {
-        Ok(Some(OsFile::from_raw_handle(winx::file::reopen_file(
+        Ok(Some(RawOsHandle::from_raw_handle(winx::file::reopen_file(
             handle,
             new_access_mode,
             fdflags.into(),
@@ -120,13 +125,13 @@ fn file_access_mode_from_fdflags(fdflags: types::Fdflags, read: bool, write: boo
 // ..       gets cookie = 2
 // other entries, in order they were returned by FindNextFileW get subsequent integers as their cookies
 pub(crate) fn readdir(
-    file: &OsFile,
+    dirfd: &OsDir,
     cookie: types::Dircookie,
 ) -> Result<Box<dyn Iterator<Item = Result<(types::Dirent, String)>>>> {
     use winx::file::get_file_path;
 
     let cookie = cookie.try_into()?;
-    let path = get_file_path(&file.as_file())?;
+    let path = get_file_path(&*dirfd.as_file()?)?;
     // std::fs::ReadDir doesn't return . and .., so we need to emulate it
     let path = Path::new(&path);
     // The directory /.. is the same as / on Unix (at least on ext4), so emulate this behavior too
diff --git a/crates/wasi-common/src/sys/windows/mod.rs b/crates/wasi-common/src/sys/windows/mod.rs
index c493115eb..109385f15 100644
--- a/crates/wasi-common/src/sys/windows/mod.rs
+++ b/crates/wasi-common/src/sys/windows/mod.rs
@@ -1,18 +1,87 @@
 pub(crate) mod clock;
 pub(crate) mod fd;
+pub(crate) mod osdir;
+pub(crate) mod osfile;
 pub(crate) mod oshandle;
+pub(crate) mod osother;
 pub(crate) mod path;
 pub(crate) mod poll;
+pub(crate) mod stdio;
 
-use crate::wasi::{types, Errno, Result};
+use crate::handle::HandleRights;
+use crate::sys::AsFile;
+use crate::wasi::{types, Errno, Result, RightsExt};
 use std::convert::{TryFrom, TryInto};
 use std::fs::File;
+use std::mem::ManuallyDrop;
+use std::os::windows::prelude::{AsRawHandle, FromRawHandle};
 use std::path::Path;
 use std::time::{SystemTime, UNIX_EPOCH};
 use std::{io, string};
 use winapi::shared::winerror;
 use winx::file::{CreationDisposition, Flags};
 
+impl<T: AsRawHandle> AsFile for T {
+    fn as_file(&self) -> io::Result<ManuallyDrop<File>> {
+        let file = unsafe { File::from_raw_handle(self.as_raw_handle()) };
+        Ok(ManuallyDrop::new(file))
+    }
+}
+
+pub(super) fn get_file_type(file: &File) -> io::Result<types::Filetype> {
+    let file_type = unsafe { winx::file::get_file_type(file.as_raw_handle())? };
+    let file_type = if file_type.is_char() {
+        // character file: LPT device or console
+        // TODO: rule out LPT device
+        types::Filetype::CharacterDevice
+    } else if file_type.is_disk() {
+        // disk file: file, dir or disk device
+        let meta = file.metadata()?;
+        if meta.is_dir() {
+            types::Filetype::Directory
+        } else if meta.is_file() {
+            types::Filetype::RegularFile
+        } else {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+    } else if file_type.is_pipe() {
+        // pipe object: socket, named pipe or anonymous pipe
+        // TODO: what about pipes, etc?
+        types::Filetype::SocketStream
+    } else {
+        return Err(io::Error::from_raw_os_error(libc::EINVAL));
+    };
+    Ok(file_type)
+}
+
+pub(super) fn get_rights(file_type: &types::Filetype) -> io::Result<HandleRights> {
+    let (base, inheriting) = match file_type {
+        types::Filetype::BlockDevice => (
+            types::Rights::block_device_base(),
+            types::Rights::block_device_inheriting(),
+        ),
+        types::Filetype::CharacterDevice => (types::Rights::tty_base(), types::Rights::tty_base()),
+        types::Filetype::SocketDgram | types::Filetype::SocketStream => (
+            types::Rights::socket_base(),
+            types::Rights::socket_inheriting(),
+        ),
+        types::Filetype::SymbolicLink | types::Filetype::Unknown => (
+            types::Rights::regular_file_base(),
+            types::Rights::regular_file_inheriting(),
+        ),
+        types::Filetype::Directory => (
+            types::Rights::directory_base(),
+            types::Rights::directory_inheriting(),
+        ),
+        types::Filetype::RegularFile => (
+            types::Rights::regular_file_base(),
+            types::Rights::regular_file_inheriting(),
+        ),
+    };
+    let rights = HandleRights::new(base, inheriting);
+    Ok(rights)
+}
+
 pub fn preopen_dir<P: AsRef<Path>>(path: P) -> io::Result<File> {
     use std::fs::OpenOptions;
     use std::os::windows::fs::OpenOptionsExt;
diff --git a/crates/wasi-common/src/sys/windows/osdir.rs b/crates/wasi-common/src/sys/windows/osdir.rs
new file mode 100644
index 000000000..80cee50bc
--- /dev/null
+++ b/crates/wasi-common/src/sys/windows/osdir.rs
@@ -0,0 +1,51 @@
+use super::oshandle::RawOsHandle;
+use crate::handle::HandleRights;
+use crate::wasi::{types, RightsExt};
+use std::cell::Cell;
+use std::convert::TryFrom;
+use std::fs::File;
+use std::io;
+use std::os::windows::prelude::{AsRawHandle, FromRawHandle, IntoRawHandle};
+
+#[derive(Debug)]
+pub(crate) struct OsDir {
+    pub(crate) rights: Cell<HandleRights>,
+    pub(crate) handle: RawOsHandle,
+}
+
+impl OsDir {
+    pub(crate) fn new(rights: HandleRights, handle: RawOsHandle) -> io::Result<Self> {
+        let rights = Cell::new(rights);
+        Ok(Self { rights, handle })
+    }
+}
+
+impl TryFrom<File> for OsDir {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let ft = file.metadata()?.file_type();
+        if !ft.is_dir() {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+        let rights = get_rights(&file)?;
+        let handle = unsafe { RawOsHandle::from_raw_handle(file.into_raw_handle()) };
+        Self::new(rights, handle)
+    }
+}
+
+fn get_rights(file: &File) -> io::Result<HandleRights> {
+    use winx::file::{query_access_information, AccessMode};
+    let mut rights = HandleRights::new(
+        types::Rights::directory_base(),
+        types::Rights::directory_inheriting(),
+    );
+    let mode = query_access_information(file.as_raw_handle())?;
+    if mode.contains(AccessMode::FILE_GENERIC_READ) {
+        rights.base |= types::Rights::FD_READ;
+    }
+    if mode.contains(AccessMode::FILE_GENERIC_WRITE) {
+        rights.base |= types::Rights::FD_WRITE;
+    }
+    Ok(rights)
+}
diff --git a/crates/wasi-common/src/sys/windows/osfile.rs b/crates/wasi-common/src/sys/windows/osfile.rs
new file mode 100644
index 000000000..0ef24c0b3
--- /dev/null
+++ b/crates/wasi-common/src/sys/windows/osfile.rs
@@ -0,0 +1,38 @@
+use super::oshandle::RawOsHandle;
+use crate::handle::HandleRights;
+use crate::sys::osfile::OsFile;
+use crate::wasi::{types, RightsExt};
+use std::convert::TryFrom;
+use std::fs::File;
+use std::io;
+use std::os::windows::prelude::{AsRawHandle, FromRawHandle, IntoRawHandle};
+
+impl TryFrom<File> for OsFile {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let ft = file.metadata()?.file_type();
+        if !ft.is_file() {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+        let rights = get_rights(&file)?;
+        let handle = unsafe { RawOsHandle::from_raw_handle(file.into_raw_handle()) };
+        Ok(Self::new(rights, handle))
+    }
+}
+
+fn get_rights(file: &File) -> io::Result<HandleRights> {
+    use winx::file::{query_access_information, AccessMode};
+    let mut rights = HandleRights::new(
+        types::Rights::regular_file_base(),
+        types::Rights::regular_file_inheriting(),
+    );
+    let mode = query_access_information(file.as_raw_handle())?;
+    if mode.contains(AccessMode::FILE_GENERIC_READ) {
+        rights.base |= types::Rights::FD_READ;
+    }
+    if mode.contains(AccessMode::FILE_GENERIC_WRITE) {
+        rights.base |= types::Rights::FD_WRITE;
+    }
+    Ok(rights)
+}
diff --git a/crates/wasi-common/src/sys/windows/oshandle.rs b/crates/wasi-common/src/sys/windows/oshandle.rs
index a313416a2..89884d03a 100644
--- a/crates/wasi-common/src/sys/windows/oshandle.rs
+++ b/crates/wasi-common/src/sys/windows/oshandle.rs
@@ -1,16 +1,19 @@
-use crate::entry::EntryRights;
-use crate::sys::oshandle::{AsFile, OsHandle, OsHandleExt};
-use crate::wasi::{types, RightsExt};
+use crate::sys::AsFile;
 use std::cell::Cell;
-use std::fs::{File, OpenOptions};
+use std::fs::File;
 use std::io;
 use std::mem::ManuallyDrop;
 use std::os::windows::prelude::{AsRawHandle, FromRawHandle, IntoRawHandle, RawHandle};
 
 #[derive(Debug)]
-pub(crate) struct OsFile(Cell<RawHandle>);
+pub(crate) struct RawOsHandle(Cell<RawHandle>);
 
-impl OsFile {
+impl RawOsHandle {
+    /// Tries cloning `self`.
+    pub(crate) fn try_clone(&self) -> io::Result<Self> {
+        let handle = self.as_file()?.try_clone()?;
+        Ok(Self(Cell::new(handle.into_raw_handle())))
+    }
     /// Consumes `other` taking the ownership of the underlying
     /// `RawHandle` file handle.
     pub(crate) fn update_from(&self, other: Self) {
@@ -22,14 +25,9 @@ impl OsFile {
             File::from_raw_handle(old_handle);
         }
     }
-    /// Clones `self`.
-    pub(crate) fn try_clone(&self) -> io::Result<Self> {
-        let handle = self.as_file().try_clone()?;
-        Ok(Self(Cell::new(handle.into_raw_handle())))
-    }
 }
 
-impl Drop for OsFile {
+impl Drop for RawOsHandle {
     fn drop(&mut self) {
         unsafe {
             File::from_raw_handle(self.as_raw_handle());
@@ -37,133 +35,22 @@ impl Drop for OsFile {
     }
 }
 
-impl AsRawHandle for OsFile {
+impl AsRawHandle for RawOsHandle {
     fn as_raw_handle(&self) -> RawHandle {
         self.0.get()
     }
 }
 
-impl FromRawHandle for OsFile {
+impl FromRawHandle for RawOsHandle {
     unsafe fn from_raw_handle(handle: RawHandle) -> Self {
         Self(Cell::new(handle))
     }
 }
 
-impl IntoRawHandle for OsFile {
+impl IntoRawHandle for RawOsHandle {
     fn into_raw_handle(self) -> RawHandle {
         // We need to prevent dropping of the OsFile
         let wrapped = ManuallyDrop::new(self);
         wrapped.0.get()
     }
 }
-
-impl AsFile for OsFile {
-    fn as_file(&self) -> ManuallyDrop<File> {
-        let file = unsafe { File::from_raw_handle(self.0.get()) };
-        ManuallyDrop::new(file)
-    }
-}
-
-impl AsRawHandle for OsHandle {
-    fn as_raw_handle(&self) -> RawHandle {
-        match self {
-            Self::OsFile(file) => file.as_raw_handle(),
-            Self::Stdin => io::stdin().as_raw_handle(),
-            Self::Stdout => io::stdout().as_raw_handle(),
-            Self::Stderr => io::stderr().as_raw_handle(),
-        }
-    }
-}
-
-impl AsFile for OsHandle {
-    fn as_file(&self) -> ManuallyDrop<File> {
-        let file = unsafe { File::from_raw_handle(self.as_raw_handle()) };
-        ManuallyDrop::new(file)
-    }
-}
-
-impl From<File> for OsHandle {
-    fn from(file: File) -> Self {
-        Self::from(unsafe { OsFile::from_raw_handle(file.into_raw_handle()) })
-    }
-}
-
-impl OsHandleExt for OsHandle {
-    fn get_file_type(&self) -> io::Result<types::Filetype> {
-        let file_type = unsafe { winx::file::get_file_type(self.as_raw_handle())? };
-        let file_type = if file_type.is_char() {
-            // character file: LPT device or console
-            // TODO: rule out LPT device
-            types::Filetype::CharacterDevice
-        } else if file_type.is_disk() {
-            // disk file: file, dir or disk device
-            let file = self.as_file();
-            let meta = file.metadata()?;
-            if meta.is_dir() {
-                types::Filetype::Directory
-            } else if meta.is_file() {
-                types::Filetype::RegularFile
-            } else {
-                return Err(io::Error::from_raw_os_error(libc::EINVAL));
-            }
-        } else if file_type.is_pipe() {
-            // pipe object: socket, named pipe or anonymous pipe
-            // TODO: what about pipes, etc?
-            types::Filetype::SocketStream
-        } else {
-            return Err(io::Error::from_raw_os_error(libc::EINVAL));
-        };
-        Ok(file_type)
-    }
-
-    fn get_rights(&self, file_type: types::Filetype) -> io::Result<EntryRights> {
-        use winx::file::{query_access_information, AccessMode};
-        let (base, inheriting) = match file_type {
-            types::Filetype::BlockDevice => (
-                types::Rights::block_device_base(),
-                types::Rights::block_device_inheriting(),
-            ),
-            types::Filetype::CharacterDevice => {
-                (types::Rights::tty_base(), types::Rights::tty_base())
-            }
-            types::Filetype::Directory => (
-                types::Rights::directory_base(),
-                types::Rights::directory_inheriting(),
-            ),
-            types::Filetype::RegularFile => (
-                types::Rights::regular_file_base(),
-                types::Rights::regular_file_inheriting(),
-            ),
-            types::Filetype::SocketDgram | types::Filetype::SocketStream => (
-                types::Rights::socket_base(),
-                types::Rights::socket_inheriting(),
-            ),
-            types::Filetype::SymbolicLink | types::Filetype::Unknown => (
-                types::Rights::regular_file_base(),
-                types::Rights::regular_file_inheriting(),
-            ),
-        };
-        let mut rights = EntryRights::new(base, inheriting);
-        match file_type {
-            types::Filetype::Directory | types::Filetype::RegularFile => {
-                let mode = query_access_information(self.as_raw_handle())?;
-                if mode.contains(AccessMode::FILE_GENERIC_READ) {
-                    rights.base |= types::Rights::FD_READ;
-                }
-                if mode.contains(AccessMode::FILE_GENERIC_WRITE) {
-                    rights.base |= types::Rights::FD_WRITE;
-                }
-            }
-            _ => {
-                // TODO: is there a way around this? On windows, it seems
-                // we cannot check access rights for anything but dirs and regular files
-            }
-        }
-        Ok(rights)
-    }
-
-    fn from_null() -> io::Result<Self> {
-        let file = OpenOptions::new().read(true).write(true).open("NUL")?;
-        Ok(Self::from(file))
-    }
-}
diff --git a/crates/wasi-common/src/sys/windows/osother.rs b/crates/wasi-common/src/sys/windows/osother.rs
new file mode 100644
index 000000000..47db73217
--- /dev/null
+++ b/crates/wasi-common/src/sys/windows/osother.rs
@@ -0,0 +1,31 @@
+use super::oshandle::RawOsHandle;
+use super::{get_file_type, get_rights};
+use crate::handle::Handle;
+use crate::sys::osother::{OsOther, OsOtherExt};
+use crate::wasi::types;
+use std::convert::TryFrom;
+use std::fs::{File, OpenOptions};
+use std::io;
+use std::os::windows::prelude::{FromRawHandle, IntoRawHandle};
+
+impl TryFrom<File> for OsOther {
+    type Error = io::Error;
+
+    fn try_from(file: File) -> io::Result<Self> {
+        let file_type = get_file_type(&file)?;
+        if file_type == types::Filetype::RegularFile || file_type == types::Filetype::Directory {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+        let rights = get_rights(&file_type)?;
+        let handle = unsafe { RawOsHandle::from_raw_handle(file.into_raw_handle()) };
+        Ok(Self::new(file_type, rights, handle))
+    }
+}
+
+impl OsOtherExt for OsOther {
+    fn from_null() -> io::Result<Box<dyn Handle>> {
+        let file = OpenOptions::new().read(true).write(true).open("NUL")?;
+        let file = Self::try_from(file)?;
+        Ok(Box::new(file))
+    }
+}
diff --git a/crates/wasi-common/src/sys/windows/path.rs b/crates/wasi-common/src/sys/windows/path.rs
index a33ffc08c..d41086671 100644
--- a/crates/wasi-common/src/sys/windows/path.rs
+++ b/crates/wasi-common/src/sys/windows/path.rs
@@ -1,7 +1,8 @@
-use super::oshandle::OsFile;
-use crate::entry::EntryRights;
-use crate::sys::oshandle::{AsFile, OsHandle};
+use crate::handle::{Handle, HandleRights};
+use crate::sys::osdir::OsDir;
+use crate::sys::AsFile;
 use crate::wasi::{types, Errno, Result};
+use std::convert::TryFrom;
 use std::ffi::{OsStr, OsString};
 use std::fs::{self, Metadata, OpenOptions};
 use std::os::windows::ffi::{OsStrExt, OsStringExt};
@@ -10,7 +11,7 @@ use std::path::{Path, PathBuf};
 use winapi::shared::winerror;
 use winx::file::AccessMode;
 
-fn strip_trailing_slashes_and_concatenate(dirfd: &OsFile, path: &str) -> Result<Option<PathBuf>> {
+fn strip_trailing_slashes_and_concatenate(dirfd: &OsDir, path: &str) -> Result<Option<PathBuf>> {
     if path.ends_with('/') {
         let suffix = path.trim_end_matches('/');
         concatenate(dirfd, Path::new(suffix)).map(Some)
@@ -28,7 +29,7 @@ fn strip_extended_prefix<P: AsRef<OsStr>>(path: P) -> OsString {
     }
 }
 
-fn concatenate<P: AsRef<Path>>(file: &OsFile, path: P) -> Result<PathBuf> {
+fn concatenate<P: AsRef<Path>>(file: &OsDir, path: P) -> Result<PathBuf> {
     use winx::file::get_file_path;
 
     // WASI is not able to deal with absolute paths
@@ -37,7 +38,7 @@ fn concatenate<P: AsRef<Path>>(file: &OsFile, path: P) -> Result<PathBuf> {
         return Err(Errno::Notcapable);
     }
 
-    let dir_path = get_file_path(&file.as_file())?;
+    let dir_path = get_file_path(&*file.as_file()?)?;
     // concatenate paths
     let mut out_path = PathBuf::from(dir_path);
     out_path.push(path.as_ref());
@@ -89,10 +90,10 @@ pub(crate) fn from_host<S: AsRef<OsStr>>(s: S) -> Result<String> {
 }
 
 pub(crate) fn open_rights(
-    input_rights: &EntryRights,
+    input_rights: &HandleRights,
     oflags: types::Oflags,
     fdflags: types::Fdflags,
-) -> EntryRights {
+) -> HandleRights {
     // which rights are needed on the dirfd?
     let mut needed_base = types::Rights::PATH_OPEN;
     let mut needed_inheriting = input_rights.base | input_rights.inheriting;
@@ -113,10 +114,10 @@ pub(crate) fn open_rights(
         needed_inheriting |= types::Rights::FD_SYNC;
     }
 
-    EntryRights::new(needed_base, needed_inheriting)
+    HandleRights::new(needed_base, needed_inheriting)
 }
 
-pub(crate) fn readlinkat(dirfd: &OsFile, s_path: &str) -> Result<String> {
+pub(crate) fn readlinkat(dirfd: &OsDir, s_path: &str) -> Result<String> {
     use winx::file::get_file_path;
 
     let path = concatenate(dirfd, Path::new(s_path))?;
@@ -126,7 +127,7 @@ pub(crate) fn readlinkat(dirfd: &OsFile, s_path: &str) -> Result<String> {
             // we need to strip the prefix from the absolute path
             // as otherwise we will error out since WASI is not capable
             // of dealing with absolute paths
-            let dir_path = get_file_path(&dirfd.as_file())?;
+            let dir_path = get_file_path(&*dirfd.as_file()?)?;
             let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
             let target_path = target_path
                 .strip_prefix(dir_path)
@@ -151,16 +152,16 @@ pub(crate) fn readlinkat(dirfd: &OsFile, s_path: &str) -> Result<String> {
     Err(err.into())
 }
 
-pub(crate) fn create_directory(file: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn create_directory(file: &OsDir, path: &str) -> Result<()> {
     let path = concatenate(file, path)?;
     std::fs::create_dir(&path)?;
     Ok(())
 }
 
 pub(crate) fn link(
-    old_dirfd: &OsFile,
+    old_dirfd: &OsDir,
     old_path: &str,
-    new_dirfd: &OsFile,
+    new_dirfd: &OsDir,
     new_path: &str,
     follow_symlinks: bool,
 ) -> Result<()> {
@@ -197,13 +198,13 @@ pub(crate) fn link(
 }
 
 pub(crate) fn open(
-    dirfd: &OsFile,
+    dirfd: &OsDir,
     path: &str,
     read: bool,
     write: bool,
     oflags: types::Oflags,
     fdflags: types::Fdflags,
-) -> Result<OsHandle> {
+) -> Result<Box<dyn Handle>> {
     use winx::file::{AccessMode, CreationDisposition, Flags};
 
     let is_trunc = oflags.contains(&types::Oflags::TRUNC);
@@ -280,11 +281,11 @@ pub(crate) fn open(
         .access_mode(access_mode.bits())
         .custom_flags(flags.bits())
         .open(&path)?;
-    let handle = OsHandle::from(file);
+    let handle = <Box<dyn Handle>>::try_from(file)?;
     Ok(handle)
 }
 
-pub(crate) fn readlink(dirfd: &OsFile, path: &str, buf: &mut [u8]) -> Result<usize> {
+pub(crate) fn readlink(dirfd: &OsDir, path: &str, buf: &mut [u8]) -> Result<usize> {
     use winx::file::get_file_path;
 
     let path = concatenate(dirfd, path)?;
@@ -294,7 +295,7 @@ pub(crate) fn readlink(dirfd: &OsFile, path: &str, buf: &mut [u8]) -> Result<usi
     // we need to strip the prefix from the absolute path
     // as otherwise we will error out since WASI is not capable
     // of dealing with absolute paths
-    let dir_path = get_file_path(&dirfd.as_file())?;
+    let dir_path = get_file_path(&*dirfd.as_file()?)?;
     let dir_path = PathBuf::from(strip_extended_prefix(dir_path));
     let target_path = target_path
         .strip_prefix(dir_path)
@@ -322,9 +323,9 @@ pub(crate) fn readlink(dirfd: &OsFile, path: &str, buf: &mut [u8]) -> Result<usi
 }
 
 pub(crate) fn rename(
-    old_dirfd: &OsFile,
+    old_dirfd: &OsDir,
     old_path_: &str,
-    new_dirfd: &OsFile,
+    new_dirfd: &OsDir,
     new_path_: &str,
 ) -> Result<()> {
     use std::fs;
@@ -390,7 +391,7 @@ pub(crate) fn rename(
     }
 }
 
-pub(crate) fn symlink(old_path: &str, new_dirfd: &OsFile, new_path_: &str) -> Result<()> {
+pub(crate) fn symlink(old_path: &str, new_dirfd: &OsDir, new_path_: &str) -> Result<()> {
     use std::os::windows::fs::{symlink_dir, symlink_file};
 
     let old_path = concatenate(new_dirfd, Path::new(old_path))?;
@@ -447,7 +448,7 @@ pub(crate) fn symlink(old_path: &str, new_dirfd: &OsFile, new_path_: &str) -> Re
     }
 }
 
-pub(crate) fn unlink_file(dirfd: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn unlink_file(dirfd: &OsDir, path: &str) -> Result<()> {
     use std::fs;
 
     let path = concatenate(dirfd, path)?;
@@ -489,7 +490,7 @@ pub(crate) fn unlink_file(dirfd: &OsFile, path: &str) -> Result<()> {
     }
 }
 
-pub(crate) fn remove_directory(dirfd: &OsFile, path: &str) -> Result<()> {
+pub(crate) fn remove_directory(dirfd: &OsDir, path: &str) -> Result<()> {
     let path = concatenate(dirfd, path)?;
     std::fs::remove_dir(&path).map_err(Into::into)
 }
diff --git a/crates/wasi-common/src/sys/windows/poll.rs b/crates/wasi-common/src/sys/windows/poll.rs
index 7e56c3982..d49524a5d 100644
--- a/crates/wasi-common/src/sys/windows/poll.rs
+++ b/crates/wasi-common/src/sys/windows/poll.rs
@@ -1,11 +1,14 @@
-use super::super::oshandle::OsHandle;
+use crate::handle::Handle;
 use crate::poll::{ClockEventData, FdEventData};
-use crate::sys::oshandle::AsFile;
+use crate::sys::osdir::OsDir;
+use crate::sys::osfile::OsFile;
+use crate::sys::osother::OsOther;
+use crate::sys::stdio::{Stderr, Stdin, Stdout};
+use crate::sys::AsFile;
 use crate::wasi::{types, Errno, Result};
 use lazy_static::lazy_static;
 use log::{debug, error, trace, warn};
 use std::convert::TryInto;
-use std::os::windows::io::AsRawHandle;
 use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError};
 use std::sync::Mutex;
 use std::thread;
@@ -141,32 +144,31 @@ fn handle_timeout_event(timeout_event: ClockEventData, events: &mut Vec<types::E
 }
 
 fn handle_rw_event(event: FdEventData, out_events: &mut Vec<types::Event>) {
-    let handle = event
-        .handle
-        .as_any()
-        .downcast_ref::<OsHandle>()
-        .expect("can poll FdEvent for OS resources only");
-    let size = match handle {
-        OsHandle::OsFile(file) => {
-            if event.r#type == types::Eventtype::FdRead {
-                file.as_file()
-                    .metadata()
-                    .map(|m| m.len())
-                    .map_err(Into::into)
-            } else {
-                // The spec is unclear what nbytes should actually be for __WASI_EVENTTYPE_FD_WRITE and
-                // the implementation on Unix just returns 0 here, so it's probably fine
-                // to do the same on Windows for now.
-                // cf. https://github.com/WebAssembly/WASI/issues/148
-                Ok(0)
-            }
-        }
+    let handle = &event.handle;
+    let size = if let Some(_) = handle.as_any().downcast_ref::<Stdin>() {
         // We return the only universally correct lower bound, see the comment later in the function.
-        OsHandle::Stdin => Ok(1),
+        Ok(1)
+    } else if let Some(_) = handle.as_any().downcast_ref::<Stdout>() {
+        // On Unix, ioctl(FIONREAD) will return 0 for stdout. Emulate the same behavior on Windows.
+        Ok(0)
+    } else if let Some(_) = handle.as_any().downcast_ref::<Stderr>() {
         // On Unix, ioctl(FIONREAD) will return 0 for stdout/stderr. Emulate the same behavior on Windows.
-        OsHandle::Stdout | OsHandle::Stderr => Ok(0),
+        Ok(0)
+    } else {
+        if event.r#type == types::Eventtype::FdRead {
+            handle
+                .as_file()
+                .and_then(|f| f.metadata())
+                .map(|m| m.len())
+                .map_err(Into::into)
+        } else {
+            // The spec is unclear what nbytes should actually be for __WASI_EVENTTYPE_FD_WRITE and
+            // the implementation on Unix just returns 0 here, so it's probably fine
+            // to do the same on Windows for now.
+            // cf. https://github.com/WebAssembly/WASI/issues/148
+            Ok(0)
+        }
     };
-
     let new_event = make_rw_event(&event, size);
     out_events.push(new_event);
 }
@@ -206,33 +208,42 @@ pub(crate) fn oneoff(
     let mut pipe_events = vec![];
 
     for event in fd_events {
-        let handle = event
-            .handle
-            .as_any()
-            .downcast_ref::<OsHandle>()
-            .expect("can poll FdEvent for OS resources only");
-        match handle {
-            OsHandle::Stdin if event.r#type == types::Eventtype::FdRead => stdin_events.push(event),
-            // stdout/stderr are always considered ready to write because there seems to
+        let handle = &event.handle;
+        if let Some(_) = handle.as_any().downcast_ref::<OsFile>() {
+            immediate_events.push(event);
+        } else if let Some(_) = handle.as_any().downcast_ref::<OsDir>() {
+            immediate_events.push(event);
+        } else if let Some(_) = handle.as_any().downcast_ref::<Stdin>() {
+            stdin_events.push(event);
+        } else if let Some(_) = handle.as_any().downcast_ref::<Stdout>() {
+            // stdout are always considered ready to write because there seems to
+            // be no way of checking if a write to stdout would block.
+            //
+            // If stdin is polled for anything else then reading, then it is also
+            // considered immediately ready, following the behavior on Linux.
+            immediate_events.push(event);
+        } else if let Some(_) = handle.as_any().downcast_ref::<Stderr>() {
+            // stderr are always considered ready to write because there seems to
             // be no way of checking if a write to stdout would block.
             //
             // If stdin is polled for anything else then reading, then it is also
             // considered immediately ready, following the behavior on Linux.
-            OsHandle::Stdin | OsHandle::Stderr | OsHandle::Stdout => immediate_events.push(event),
-            OsHandle::OsFile(file) => {
-                let ftype = unsafe { winx::file::get_file_type(file.as_raw_handle()) }?;
-                if ftype.is_unknown() || ftype.is_char() {
-                    debug!("poll_oneoff: unsupported file type: {:?}", ftype);
-                    handle_error_event(event, Errno::Notsup, events);
-                } else if ftype.is_disk() {
-                    immediate_events.push(event);
-                } else if ftype.is_pipe() {
-                    pipe_events.push(event);
-                } else {
-                    unreachable!();
-                }
+            immediate_events.push(event);
+        } else if let Some(other) = handle.as_any().downcast_ref::<OsOther>() {
+            if other.get_file_type() == types::Filetype::SocketStream {
+                // We map pipe to SocketStream
+                pipe_events.push(event);
+            } else {
+                debug!(
+                    "poll_oneoff: unsupported file type: {}",
+                    other.get_file_type()
+                );
+                handle_error_event(event, Errno::Notsup, events);
             }
-        };
+        } else {
+            log::error!("can poll FdEvent for OS resources only");
+            return Err(Errno::Badf);
+        }
     }
 
     let immediate = !immediate_events.is_empty();
diff --git a/crates/wasi-common/src/sys/windows/stdio.rs b/crates/wasi-common/src/sys/windows/stdio.rs
new file mode 100644
index 000000000..25140db90
--- /dev/null
+++ b/crates/wasi-common/src/sys/windows/stdio.rs
@@ -0,0 +1,59 @@
+use super::{get_file_type, get_rights};
+use crate::handle::Handle;
+use crate::sys::stdio::{Stderr, StderrExt, Stdin, StdinExt, Stdout, StdoutExt};
+use std::cell::Cell;
+use std::fs::File;
+use std::io;
+use std::mem::ManuallyDrop;
+use std::os::windows::prelude::{AsRawHandle, FromRawHandle, RawHandle};
+
+impl AsRawHandle for Stdin {
+    fn as_raw_handle(&self) -> RawHandle {
+        io::stdin().as_raw_handle()
+    }
+}
+
+impl AsRawHandle for Stdout {
+    fn as_raw_handle(&self) -> RawHandle {
+        io::stdout().as_raw_handle()
+    }
+}
+
+impl AsRawHandle for Stderr {
+    fn as_raw_handle(&self) -> RawHandle {
+        io::stderr().as_raw_handle()
+    }
+}
+
+impl StdinExt for Stdin {
+    fn stdin() -> io::Result<Box<dyn Handle>> {
+        let file = unsafe { File::from_raw_handle(io::stdin().as_raw_handle()) };
+        let file = ManuallyDrop::new(file);
+        let file_type = get_file_type(&file)?;
+        let rights = get_rights(&file_type)?;
+        let rights = Cell::new(rights);
+        Ok(Box::new(Self { file_type, rights }))
+    }
+}
+
+impl StdoutExt for Stdout {
+    fn stdout() -> io::Result<Box<dyn Handle>> {
+        let file = unsafe { File::from_raw_handle(io::stdin().as_raw_handle()) };
+        let file = ManuallyDrop::new(file);
+        let file_type = get_file_type(&file)?;
+        let rights = get_rights(&file_type)?;
+        let rights = Cell::new(rights);
+        Ok(Box::new(Self { file_type, rights }))
+    }
+}
+
+impl StderrExt for Stderr {
+    fn stderr() -> io::Result<Box<dyn Handle>> {
+        let file = unsafe { File::from_raw_handle(io::stdin().as_raw_handle()) };
+        let file = ManuallyDrop::new(file);
+        let file_type = get_file_type(&file)?;
+        let rights = get_rights(&file_type)?;
+        let rights = Cell::new(rights);
+        Ok(Box::new(Self { file_type, rights }))
+    }
+}
diff --git a/crates/wasi-common/src/virtfs.rs b/crates/wasi-common/src/virtfs.rs
index 521119f42..c868a2795 100644
--- a/crates/wasi-common/src/virtfs.rs
+++ b/crates/wasi-common/src/virtfs.rs
@@ -1,5 +1,4 @@
-use crate::entry::EntryRights;
-use crate::handle::Handle;
+use crate::handle::{Handle, HandleRights};
 use crate::wasi::{self, types, Errno, Result, RightsExt};
 use log::trace;
 use std::any::Any;
@@ -134,6 +133,7 @@ impl VecFileContents {
 /// a filesystem wherein a file descriptor is one view into a possibly-shared underlying collection
 /// of data and permissions on a filesystem.
 pub struct InMemoryFile {
+    rights: Cell<HandleRights>,
     cursor: Cell<types::Filesize>,
     parent: Rc<RefCell<Option<Box<dyn Handle>>>>,
     fd_flags: Cell<types::Fdflags>,
@@ -142,16 +142,17 @@ pub struct InMemoryFile {
 
 impl InMemoryFile {
     pub fn memory_backed() -> Self {
-        Self {
-            cursor: Cell::new(0),
-            parent: Rc::new(RefCell::new(None)),
-            fd_flags: Cell::new(types::Fdflags::empty()),
-            data: Rc::new(RefCell::new(Box::new(VecFileContents::new()))),
-        }
+        Self::new(Box::new(VecFileContents::new()))
     }
 
     pub fn new(contents: Box<dyn FileContents>) -> Self {
+        let rights = HandleRights::new(
+            types::Rights::regular_file_base(),
+            types::Rights::regular_file_inheriting(),
+        );
+        let rights = Cell::new(rights);
         Self {
+            rights,
             cursor: Cell::new(0),
             fd_flags: Cell::new(types::Fdflags::empty()),
             parent: Rc::new(RefCell::new(None)),
@@ -172,20 +173,21 @@ impl Handle for InMemoryFile {
     }
     fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
         Ok(Box::new(Self {
+            rights: self.rights.clone(),
             cursor: Cell::new(0),
             fd_flags: self.fd_flags.clone(),
             parent: Rc::clone(&self.parent),
             data: Rc::clone(&self.data),
         }))
     }
-    fn get_file_type(&self) -> io::Result<types::Filetype> {
-        Ok(types::Filetype::RegularFile)
+    fn get_file_type(&self) -> types::Filetype {
+        types::Filetype::RegularFile
     }
-    fn get_rights(&self) -> io::Result<EntryRights> {
-        Ok(EntryRights::new(
-            types::Rights::regular_file_base(),
-            types::Rights::regular_file_inheriting(),
-        ))
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, rights: HandleRights) {
+        self.rights.set(rights)
     }
     // FdOps
     fn advise(
@@ -227,7 +229,7 @@ impl Handle for InMemoryFile {
             atim: 0,
             ctim: 0,
             mtim: 0,
-            filetype: self.get_file_type()?,
+            filetype: self.get_file_type(),
         };
         Ok(stat)
     }
@@ -280,11 +282,7 @@ impl Handle for InMemoryFile {
 
         Ok(self.cursor.get())
     }
-    fn write_vectored(&self, iovs: &[io::IoSlice], isatty: bool) -> Result<usize> {
-        if isatty {
-            unimplemented!("writes to virtual tty");
-        }
-
+    fn write_vectored(&self, iovs: &[io::IoSlice]) -> Result<usize> {
         trace!("write_vectored(iovs={:?})", iovs);
         let mut data = self.data.borrow_mut();
 
@@ -402,6 +400,7 @@ impl Handle for InMemoryFile {
 
 /// A clonable read/write directory.
 pub struct VirtualDir {
+    rights: Cell<HandleRights>,
     writable: bool,
     // All copies of this `VirtualDir` must share `parent`, and changes in one copy's `parent`
     // must be reflected in all handles, so they share `Rc` of an underlying `parent`.
@@ -411,7 +410,13 @@ pub struct VirtualDir {
 
 impl VirtualDir {
     pub fn new(writable: bool) -> Self {
+        let rights = HandleRights::new(
+            types::Rights::directory_base(),
+            types::Rights::directory_inheriting(),
+        );
+        let rights = Cell::new(rights);
         Self {
+            rights,
             writable,
             parent: Rc::new(RefCell::new(None)),
             entries: Rc::new(RefCell::new(HashMap::new())),
@@ -468,19 +473,20 @@ impl Handle for VirtualDir {
     }
     fn try_clone(&self) -> io::Result<Box<dyn Handle>> {
         Ok(Box::new(Self {
+            rights: self.rights.clone(),
             writable: self.writable,
             parent: Rc::clone(&self.parent),
             entries: Rc::clone(&self.entries),
         }))
     }
-    fn get_file_type(&self) -> io::Result<types::Filetype> {
-        Ok(types::Filetype::Directory)
+    fn get_file_type(&self) -> types::Filetype {
+        types::Filetype::Directory
     }
-    fn get_rights(&self) -> io::Result<EntryRights> {
-        Ok(EntryRights::new(
-            types::Rights::directory_base(),
-            types::Rights::directory_inheriting(),
-        ))
+    fn get_rights(&self) -> HandleRights {
+        self.rights.get()
+    }
+    fn set_rights(&self, rights: HandleRights) {
+        self.rights.set(rights)
     }
     // FdOps
     fn filestat_get(&self) -> Result<types::Filestat> {
@@ -492,7 +498,7 @@ impl Handle for VirtualDir {
             atim: 0,
             ctim: 0,
             mtim: 0,
-            filetype: self.get_file_type()?,
+            filetype: self.get_file_type(),
         };
         Ok(stat)
     }
@@ -555,7 +561,7 @@ impl Handle for VirtualDir {
                 let dirent = || -> Result<types::Dirent> {
                     let dirent = types::Dirent {
                         d_namlen: name.len().try_into()?,
-                        d_type: file.get_file_type()?,
+                        d_type: file.get_file_type(),
                         d_ino: 0,
                         d_next: self.start as u64,
                     };
@@ -639,7 +645,7 @@ impl Handle for VirtualDir {
                 }
 
                 if oflags.contains(&types::Oflags::DIRECTORY)
-                    && e.get().get_file_type()? != types::Filetype::Directory
+                    && e.get().get_file_type() != types::Filetype::Directory
                 {
                     log::trace!(
                         "VirtualDir::openat was passed oflags DIRECTORY, but {:?} is a file.",
@@ -683,7 +689,7 @@ impl Handle for VirtualDir {
         match entries.entry(Path::new(trimmed_path).to_path_buf()) {
             Entry::Occupied(e) => {
                 // first, does this name a directory?
-                if e.get().get_file_type()? != types::Filetype::Directory {
+                if e.get().get_file_type() != types::Filetype::Directory {
                     return Err(Errno::Notdir);
                 }
 
@@ -731,7 +737,7 @@ impl Handle for VirtualDir {
         match entries.entry(Path::new(trimmed_path).to_path_buf()) {
             Entry::Occupied(e) => {
                 // Directories must be removed through `remove_directory`, not `unlink_file`.
-                if e.get().get_file_type()? == types::Filetype::Directory {
+                if e.get().get_file_type() == types::Filetype::Directory {
                     return Err(Errno::Isdir);
                 }
 


Test patch:
diff --git a/cranelift/filetests/Cargo.toml b/cranelift/filetests/Cargo.toml
index 7defcea92..481401cf8 100644
--- a/cranelift/filetests/Cargo.toml
+++ b/cranelift/filetests/Cargo.toml
@@ -12,6 +12,7 @@ edition = "2018"
 [dependencies]
 cranelift-codegen = { path = "../codegen", version = "0.63.0", features = ["testing_hooks"] }
 cranelift-frontend = { path = "../frontend", version = "0.63.0" }
+cranelift-interpreter = { path = "../interpreter", version = "*" }
 cranelift-native = { path = "../native", version = "0.63.0" }
 cranelift-reader = { path = "../reader", version = "0.63.0" }
 cranelift-preopt = { path = "../preopt", version = "0.63.0" }
diff --git a/cranelift/filetests/filetests/interpreter/add.clif b/cranelift/filetests/filetests/interpreter/add.clif
new file mode 100644
index 000000000..d95298ad8
--- /dev/null
+++ b/cranelift/filetests/filetests/interpreter/add.clif
@@ -0,0 +1,13 @@
+test interpret
+
+function %add() -> i32 {
+block0:
+    v0 = iconst.i32 40
+    v1 = iconst.i32 2
+    v2 = iadd v0, v1
+    jump block1(v2)
+
+block1(v3: i32):
+    return v3
+}
+; run: %add() == 42
diff --git a/cranelift/filetests/filetests/interpreter/fibonacci.clif b/cranelift/filetests/filetests/interpreter/fibonacci.clif
new file mode 100644
index 000000000..70515c4b2
--- /dev/null
+++ b/cranelift/filetests/filetests/interpreter/fibonacci.clif
@@ -0,0 +1,64 @@
+test interpret
+
+; A non-recursive fibonacci implementation.
+function %fibonacci(i32) -> i32 {
+block0(v0: i32):
+    v1 = icmp_imm ule v0, 2
+    v2 = iconst.i32 1
+    brnz v1, block3(v2) ; handle base case, n <= 2
+    fallthrough block1(v0, v2)
+
+block1(v4: i32, v5:i32):
+    v6 = iconst.i32 1
+    v7 = irsub_imm v4, 2
+    fallthrough block2(v7, v5, v6)
+
+block2(v10: i32, v11: i32, v12: i32): ; params: n, fib(n-1), fib(n-2)
+    v13 = iadd v11, v12
+    v14 = irsub_imm v10, 1
+    v15 = icmp_imm eq v14, 0
+    brnz v15, block3(v13)
+    jump block2(v14, v13, v11)
+
+block3(v20: i32): ; early return and end of loop
+    return v20
+}
+; run: %fibonacci(0) == 1
+; run: %fibonacci(1) == 1
+; run: %fibonacci(2) == 1
+; run: %fibonacci(3) == 2
+; run: %fibonacci(4) == 3
+; run: %fibonacci(5) == 5
+; run: %fibonacci(6) == 8
+; run: %fibonacci(10) == 55
+
+
+; A recursive fibonacci implementation.
+function %fibonacci_recursive(i32) -> i32 {
+    fn0 = %fibonacci_recursive(i32) -> i32
+
+block0(v0: i32):
+    v1 = icmp_imm ule v0, 2
+    brnz v1, block2
+    fallthrough block1(v0)
+
+block1(v10: i32):
+    v11 = irsub_imm v10, 1
+    v12 = call fn0(v11)
+    v13 = irsub_imm v10, 2
+    v14 = call fn0(v13)
+    v15 = iadd v12, v14
+    return v15
+
+block2:
+    v20 = iconst.i32 1
+    return v20
+}
+; run: %fibonacci_recurs(0) == 1
+; run: %fibonacci_recurs(1) == 1
+; run: %fibonacci_recurs(2) == 1
+; run: %fibonacci_recurs(3) == 2
+; run: %fibonacci_recurs(4) == 3
+; run: %fibonacci_recurs(5) == 5
+; run: %fibonacci_recurs(6) == 8
+; run: %fibonacci_recurs(10) == 55
diff --git a/cranelift/filetests/src/lib.rs b/cranelift/filetests/src/lib.rs
index 7b831ebb5..bc1a6df1e 100644
--- a/cranelift/filetests/src/lib.rs
+++ b/cranelift/filetests/src/lib.rs
@@ -42,6 +42,7 @@ mod test_cat;
 mod test_compile;
 mod test_dce;
 mod test_domtree;
+mod test_interpret;
 mod test_legalizer;
 mod test_licm;
 mod test_postopt;
@@ -122,23 +123,24 @@ fn new_subtest(parsed: &TestCommand) -> subtest::SubtestResult<Box<dyn subtest::
         "binemit" => test_binemit::subtest(parsed),
         "cat" => test_cat::subtest(parsed),
         "compile" => test_compile::subtest(parsed),
-        "rodata" => test_rodata::subtest(parsed),
         "dce" => test_dce::subtest(parsed),
         "domtree" => test_domtree::subtest(parsed),
+        "interpret" => test_interpret::subtest(parsed),
         "legalizer" => test_legalizer::subtest(parsed),
         "licm" => test_licm::subtest(parsed),
         "postopt" => test_postopt::subtest(parsed),
-        "simple_preopt" => test_simple_preopt::subtest(parsed),
+        "preopt" => test_preopt::subtest(parsed),
         "print-cfg" => test_print_cfg::subtest(parsed),
         "regalloc" => test_regalloc::subtest(parsed),
+        "rodata" => test_rodata::subtest(parsed),
         "run" => test_run::subtest(parsed),
+        "safepoint" => test_safepoint::subtest(parsed),
         "shrink" => test_shrink::subtest(parsed),
         "simple-gvn" => test_simple_gvn::subtest(parsed),
+        "simple_preopt" => test_simple_preopt::subtest(parsed),
+        "unwind" => test_unwind::subtest(parsed),
         "vcode" => test_vcode::subtest(parsed),
         "verifier" => test_verifier::subtest(parsed),
-        "preopt" => test_preopt::subtest(parsed),
-        "safepoint" => test_safepoint::subtest(parsed),
-        "unwind" => test_unwind::subtest(parsed),
         _ => Err(format!("unknown test command '{}'", parsed.command)),
     }
 }
diff --git a/cranelift/filetests/src/test_interpret.rs b/cranelift/filetests/src/test_interpret.rs
new file mode 100644
index 000000000..ccde4a008
--- /dev/null
+++ b/cranelift/filetests/src/test_interpret.rs
@@ -0,0 +1,61 @@
+//! Test command for interpreting CLIF files and verifying their results
+//!
+//! The `interpret` test command interprets each function on the host machine using [RunCommand]s.
+
+use crate::subtest::{Context, SubTest, SubtestResult};
+use cranelift_codegen::{self, ir};
+use cranelift_interpreter::environment::Environment;
+use cranelift_interpreter::interpreter::{ControlFlow, Interpreter};
+use cranelift_reader::{parse_run_command, TestCommand};
+use log::trace;
+use std::borrow::Cow;
+
+struct TestInterpret;
+
+pub fn subtest(parsed: &TestCommand) -> SubtestResult<Box<dyn SubTest>> {
+    assert_eq!(parsed.command, "interpret");
+    if !parsed.options.is_empty() {
+        Err(format!("No options allowed on {}", parsed))
+    } else {
+        Ok(Box::new(TestInterpret))
+    }
+}
+
+impl SubTest for TestInterpret {
+    fn name(&self) -> &'static str {
+        "interpret"
+    }
+
+    fn is_mutating(&self) -> bool {
+        false
+    }
+
+    fn needs_isa(&self) -> bool {
+        false
+    }
+
+    fn run(&self, func: Cow<ir::Function>, context: &Context) -> SubtestResult<()> {
+        for comment in context.details.comments.iter() {
+            if let Some(command) =
+                parse_run_command(comment.text, &func.signature).map_err(|e| e.to_string())?
+            {
+                trace!("Parsed run command: {}", command);
+
+                let mut env = Environment::default();
+                env.add(func.name.to_string(), func.clone().into_owned());
+                let interpreter = Interpreter::new(env);
+
+                command.run(|func_name, args| {
+                    // Because we have stored function names with a leading %, we need to re-add it.
+                    let func_name = &format!("%{}", func_name);
+                    match interpreter.call_by_name(func_name, args) {
+                        Ok(ControlFlow::Return(results)) => Ok(results),
+                        Ok(_) => panic!("Unexpected returned control flow--this is likely a bug."),
+                        Err(t) => Err(t.to_string()),
+                    }
+                })?;
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/cranelift/filetests/src/test_run.rs b/cranelift/filetests/src/test_run.rs
index aac39b6fe..0044779ba 100644
--- a/cranelift/filetests/src/test_run.rs
+++ b/cranelift/filetests/src/test_run.rs
@@ -59,7 +59,7 @@ impl SubTest for TestRun {
                 let compiled_fn = compiler
                     .compile(func.clone().into_owned())
                     .map_err(|e| e.to_string())?;
-                command.run(|args| compiled_fn.call(args))?;
+                command.run(|_, args| Ok(compiled_fn.call(args)))?;
             }
         }
         Ok(())