Bläddra i källkod

Started updating btfuse to use LocalFs.

Matthew Carr 2 år sedan
förälder
incheckning
1c59d921b4

+ 44 - 3
Cargo.lock

@@ -80,6 +80,17 @@ version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
 
+[[package]]
+name = "async-trait"
+version = "0.1.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -209,12 +220,23 @@ dependencies = [
  "btserde",
  "bytes",
  "fuse-backend-rs",
- "lazy_static",
  "libc",
  "log",
  "paste",
  "positioned-io",
  "serde",
+ "tokio",
+]
+
+[[package]]
+name = "btfproto-tests"
+version = "0.1.0"
+dependencies = [
+ "btfproto",
+ "btlib",
+ "bytes",
+ "lazy_static",
+ "libc",
  "tempdir",
  "tokio",
 ]
@@ -233,13 +255,21 @@ dependencies = [
 name = "btfuse"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
+ "btfproto",
+ "btfproto-tests",
  "btlib",
+ "bytes",
+ "ctor",
  "env_logger",
  "fuse-backend-rs",
+ "futures",
  "libc",
  "log",
+ "serde_json",
  "swtpm-harness",
  "tempdir",
+ "tokio",
  "tss-esapi",
 ]
 
@@ -264,6 +294,7 @@ dependencies = [
  "log",
  "openssl",
  "positioned-io",
+ "safemem",
  "serde",
  "serde-big-array",
  "static_assertions",
@@ -706,6 +737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "994a3bfb694ee52bf8f3bca80d784b723f150810998219337e429cc5dbe92717"
 dependencies = [
  "arc-swap",
+ "async-trait",
  "bitflags",
  "caps",
  "core-foundation-sys",
@@ -718,6 +750,7 @@ dependencies = [
  "scoped-tls",
  "slab",
  "socket2",
+ "tokio",
  "tokio-uring",
  "vm-memory",
  "vmm-sys-util",
@@ -1626,6 +1659,12 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
 
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
 [[package]]
 name = "same-file"
 version = "1.0.6"
@@ -1754,9 +1793,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.89"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
 dependencies = [
  "itoa",
  "ryu",
@@ -1860,6 +1899,7 @@ dependencies = [
 name = "swtpm-harness"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "dbus",
  "log",
  "nix 0.25.0",
@@ -1997,6 +2037,7 @@ dependencies = [
  "libc",
  "memchr",
  "mio",
+ "num_cpus",
  "pin-project-lite",
  "socket2",
  "tokio-macros",

+ 1 - 0
Cargo.toml

@@ -5,6 +5,7 @@ members = [
     "crates/btserde",
     "crates/swtpm-harness",
     "crates/btfproto",
+    "crates/btfproto-tests",
     "crates/btfuse",
     "crates/btfs",
 ]

+ 15 - 0
crates/btfproto-tests/Cargo.toml

@@ -0,0 +1,15 @@
+[package]
+name = "btfproto-tests"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btlib = { path = "../btlib" }
+btfproto = { path = "../btfproto" }
+tempdir = { version = "0.3.7" }
+lazy_static = { version = "1.4.0" }
+bytes = { version = "1.3.0" }
+libc = { version = "0.2.137" }
+tokio = { version = "1.24.2" }

+ 30 - 0
crates/btfproto-tests/src/lib.rs

@@ -0,0 +1,30 @@
+pub mod local_fs;
+pub mod mode_authorizer;
+
+use btlib::{
+    crypto::{ConcreteCreds, Creds},
+    Epoch, Principaled,
+};
+use core::time::Duration;
+use lazy_static::lazy_static;
+
+lazy_static! {
+    static ref ROOT_CREDS: ConcreteCreds = ConcreteCreds::generate().unwrap();
+    static ref NODE_CREDS: ConcreteCreds = {
+        let root_creds = &ROOT_CREDS;
+        let mut node_creds = ConcreteCreds::generate().unwrap();
+        let writecap = root_creds
+            .issue_writecap(
+                node_creds.principal(),
+                vec![],
+                Epoch::now() + Duration::from_secs(3600),
+            )
+            .unwrap();
+        node_creds.set_writecap(writecap);
+        node_creds
+    };
+}
+
+pub fn node_creds() -> &'static ConcreteCreds {
+    &NODE_CREDS
+}

+ 803 - 0
crates/btfproto-tests/src/local_fs.rs

@@ -0,0 +1,803 @@
+use super::node_creds;
+
+use btfproto::local_fs::{LocalFs, ModeAuthorizer};
+use btlib::{
+    crypto::{ConcreteCreds, CredsPriv},
+    BlockPath,
+};
+use std::sync::Arc;
+use tempdir::TempDir;
+
+pub fn bind_path<C: CredsPriv>(creds: C) -> BlockPath {
+    let writecap = creds
+        .writecap()
+        .ok_or(btlib::BlockError::MissingWritecap)
+        .unwrap();
+    writecap.bind_path()
+}
+
+pub type ConcreteFs = LocalFs<ConcreteCreds, ModeAuthorizer>;
+
+pub struct LocalFsTest {
+    dir: TempDir,
+    fs: ConcreteFs,
+    from: Arc<BlockPath>,
+}
+
+impl LocalFsTest {
+    pub fn new_empty() -> LocalFsTest {
+        let dir = TempDir::new("fuse").expect("failed to create temp dir");
+        let fs = LocalFs::new_empty(dir.path().to_owned(), 0, Self::creds(), ModeAuthorizer {})
+            .expect("failed to create empty blocktree");
+        let from = Arc::new(bind_path(node_creds()));
+        LocalFsTest { dir, fs, from }
+    }
+
+    pub fn new_existing(dir: TempDir) -> LocalFsTest {
+        let fs = LocalFs::new_existing(dir.path().to_owned(), Self::creds(), ModeAuthorizer {})
+            .expect("failed to create blocktree from existing directory");
+        let from = Arc::new(bind_path(node_creds()));
+        LocalFsTest { dir, fs, from }
+    }
+
+    pub fn into_parts(self) -> (TempDir, ConcreteFs, Arc<BlockPath>) {
+        (self.dir, self.fs, self.from)
+    }
+
+    pub fn fs(&self) -> &ConcreteFs {
+        &self.fs
+    }
+
+    pub fn creds() -> ConcreteCreds {
+        node_creds().clone()
+    }
+
+    pub fn from(&self) -> &Arc<BlockPath> {
+        &self.from
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use btfproto::{local_fs::Error, msg::*, server::FsProvider, Inode};
+    use btlib::{Result, SECTOR_SZ_DEFAULT};
+    use bytes::BytesMut;
+    use std::{
+        io::{self, Cursor, Write as IoWrite},
+        sync::Arc,
+    };
+
+    /// Tests that a new file can be created, written to and the written data can be read from it.
+    #[tokio::test]
+    async fn create_write_read() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+        let name = "README.md";
+        let flags = Flags::new(libc::O_RDWR);
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags,
+            mode: libc::S_IFREG | 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+
+        const LEN: usize = 32;
+        let expected = [1u8; LEN];
+        let WriteReply { written, .. } = bt
+            .write(
+                from,
+                inode,
+                handle,
+                0,
+                expected.len() as u64,
+                expected.as_slice(),
+            )
+            .await
+            .unwrap();
+        assert_eq!(LEN as u64, written);
+
+        let mut actual = [0u8; LEN];
+        let read_msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: LEN as u64,
+        };
+        bt.read(from, read_msg, |data| actual.copy_from_slice(data))
+            .unwrap();
+
+        assert_eq!(expected, actual)
+    }
+
+    #[tokio::test]
+    async fn lookup() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+
+        let name = "README.md";
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: Flags::default(),
+            mode: 0,
+            umask: 0,
+        };
+        let create_reply = bt.create(from, create_msg).await.unwrap();
+
+        let lookup_msg = Lookup {
+            parent: SpecInodes::RootDir.into(),
+            name,
+        };
+        let lookup_reply = bt.lookup(from, lookup_msg).await.unwrap();
+
+        assert_eq!(create_reply.inode, lookup_reply.inode);
+    }
+
+    /// Tests that data written by one instance of [Blocktree] can be read by a subsequent
+    /// instance.
+    #[tokio::test]
+    async fn new_existing() {
+        const EXPECTED: &[u8] = b"cool as cucumbers";
+        let name = "RESIGNATION.docx";
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+        let flags = Flags::new(libc::O_RDWR);
+
+        {
+            let create_msg = Create {
+                parent: SpecInodes::RootDir.into(),
+                name,
+                mode: libc::S_IFREG | 0o644,
+                flags,
+                umask: 0,
+            };
+            let CreateReply { handle, inode, .. } = bt.create(from, create_msg).await.unwrap();
+
+            let WriteReply { written, .. } = bt
+                .write(from, inode, handle, 0, EXPECTED.len() as u64, EXPECTED)
+                .await
+                .unwrap();
+            assert_eq!(EXPECTED.len() as u64, written);
+
+            let flush_msg = Flush { inode, handle };
+            bt.flush(from, flush_msg).await.unwrap();
+        }
+
+        let case = LocalFsTest::new_existing(case.dir);
+        let from = case.from();
+        let bt = &case.fs;
+
+        let lookup_msg = Lookup {
+            parent: SpecInodes::RootDir.into(),
+            name,
+        };
+        let LookupReply { inode, .. } = bt.lookup(from, lookup_msg).await.unwrap();
+
+        let open_msg = Open {
+            inode,
+            flags: Flags::new(libc::O_RDONLY),
+        };
+        let OpenReply { handle, .. } = bt.open(from, open_msg).await.unwrap();
+
+        let mut actual = BytesMut::new();
+        let read_msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: EXPECTED.len() as u64,
+        };
+        bt.read(from, read_msg, |data| actual.extend_from_slice(data))
+            .unwrap();
+
+        assert_eq!(EXPECTED, &actual)
+    }
+
+    /// Tests that an error is returned by the `Blocktree::write` method if the file was opened
+    /// read-only.
+    #[tokio::test]
+    async fn open_read_only_write_is_error() {
+        let name = "books.ods";
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: Flags::default(),
+            mode: libc::S_IFREG | 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+
+        let close_msg = Close { inode, handle };
+        bt.close(from, close_msg).await.unwrap();
+
+        let open_msg = Open {
+            inode,
+            flags: libc::O_RDONLY.into(),
+        };
+        let OpenReply { handle, .. } = bt.open(from, open_msg).await.unwrap();
+
+        let data = [1u8; 32];
+        let result = bt
+            .write(from, inode, handle, 0, data.len() as u64, data.as_slice())
+            .await;
+
+        let err = result.err().unwrap();
+        let err = err.downcast::<Error>().unwrap();
+        let actual_handle = if let Error::ReadOnlyHandle(actual_handle) = err {
+            Some(actual_handle)
+        } else {
+            None
+        };
+        assert_eq!(Some(handle), actual_handle);
+    }
+
+    /// Asserts that the given [Result] is an [Err] and that it contains an [io::Error] which
+    /// corresponds to the [libc::ENOENT] error code.
+    fn assert_enoent<T>(result: Result<T>) {
+        let err = result.err().unwrap().downcast::<io::Error>().unwrap();
+        assert_eq!(libc::ENOENT, err.raw_os_error().unwrap());
+    }
+
+    /// Tests that multiple handles see consistent metadata associated with a block.
+    #[tokio::test]
+    async fn ensure_metadata_consistency() {
+        let case = LocalFsTest::new_empty();
+        let from = case.from();
+        let trash = ".Trash";
+        let file = "file.txt";
+        let bt = &case.fs;
+        let root = SpecInodes::RootDir.into();
+
+        let open_msg = Open {
+            inode: root,
+            flags: libc::O_DIRECTORY.into(),
+        };
+        let OpenReply { handle, .. } = bt.open(from, open_msg).await.unwrap();
+
+        // Because the directory is open, this will cause a new handle for this block to be opened.
+        let lookup_msg = Lookup {
+            parent: root,
+            name: trash,
+        };
+        let result = bt.lookup(from, lookup_msg).await;
+        assert_enoent(result);
+
+        let close_msg = Close {
+            inode: root,
+            handle,
+        };
+        bt.close(from, close_msg).await.unwrap();
+
+        let create_msg = Create {
+            parent: root,
+            name: file,
+            flags: Flags::default(),
+            mode: libc::S_IFREG | 0o644,
+            umask: 0,
+        };
+        bt.create(from, create_msg).await.unwrap();
+
+        let open_msg = Open {
+            inode: root,
+            flags: libc::O_DIRECTORY.into(),
+        };
+        bt.open(from, open_msg).await.unwrap();
+
+        // Since the directory is open, the second handle will be used for this lookup.
+        let lookup_msg = Lookup {
+            parent: root,
+            name: trash,
+        };
+        let result = bt.lookup(from, lookup_msg).await;
+        assert!(result.is_err());
+    }
+
+    /// Tests that the `size` parameter actually limits the number of read bytes.
+    #[tokio::test]
+    async fn read_with_smaller_size() {
+        const DATA: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
+        let case = LocalFsTest::new_empty();
+        let from = case.from();
+        let file = "file.txt";
+        let bt = &case.fs;
+        let root: Inode = SpecInodes::RootDir.into();
+
+        let create_msg = Create {
+            parent: root,
+            name: file,
+            flags: libc::O_RDWR.into(),
+            mode: libc::S_IFREG | 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+        let WriteReply { written, .. } = bt
+            .write(from, inode, handle, 0, DATA.len() as u64, DATA.as_slice())
+            .await
+            .unwrap();
+        assert_eq!(DATA.len() as u64, written);
+        const SIZE: usize = DATA.len() / 2;
+        let mut actual = Vec::with_capacity(SIZE);
+        let read_msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: SIZE as u64,
+        };
+        bt.read(from, read_msg, |data| actual.extend_from_slice(data))
+            .unwrap();
+
+        assert_eq!(&[0, 1, 2, 3], actual.as_slice());
+    }
+
+    /// Returns an integer array starting at the given value and increasing by one for each
+    /// subsequent entry.
+    pub const fn integer_array<const N: usize>(start: u8) -> [u8; N] {
+        let mut array = [0u8; N];
+        let mut k = 0usize;
+        while k < N {
+            array[k] = start.wrapping_add(k as u8);
+            k += 1;
+        }
+        array
+    }
+
+    #[tokio::test]
+    async fn concurrent_reads() {
+        // The size of each of the reads.
+        const SIZE: usize = 4;
+        // The number of concurrent reads.
+        const NREADS: usize = 32;
+        const DATA_LEN: usize = SIZE * NREADS;
+        const DATA: [u8; DATA_LEN] = integer_array::<DATA_LEN>(0);
+        let case = LocalFsTest::new_empty();
+        let from = case.from();
+        let file = "file.txt";
+        let bt = &case.fs;
+        let root: Inode = SpecInodes::RootDir.into();
+        let mode = libc::S_IFREG | 0o644;
+
+        let create_msg = Create {
+            parent: root,
+            name: file,
+            flags: libc::O_RDWR.into(),
+            mode,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+        let WriteReply { written, .. } = bt
+            .write(from, inode, handle, 0, DATA.len() as u64, DATA.as_slice())
+            .await
+            .unwrap();
+        assert_eq!(DATA.len() as u64, written);
+        let case = Box::new(case);
+        let cb = Arc::new(Box::new(move |offset: usize| {
+            // Notice that we have concurrent reads to different offsets using the same handle.
+            // Without proper synchronization, this shouldn't work.
+            let mut actual = Vec::with_capacity(SIZE);
+            let read_msg = Read {
+                inode,
+                handle,
+                offset: offset as u64,
+                size: SIZE as u64,
+            };
+            case.fs
+                .read(case.from(), read_msg, |data| actual.extend_from_slice(data))
+                .unwrap();
+            let expected = integer_array::<SIZE>(offset as u8);
+            assert_eq!(&expected, actual.as_slice());
+        }));
+
+        let mut handles = Vec::with_capacity(NREADS);
+        for offset in (0..NREADS).map(|e| e * SIZE) {
+            let thread_cb = cb.clone();
+            handles.push(std::thread::spawn(move || thread_cb(offset)));
+        }
+        for handle in handles {
+            handle.join().unwrap();
+        }
+    }
+
+    #[tokio::test]
+    async fn rename_in_same_directory() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+        let root: Inode = SpecInodes::RootDir.into();
+        let src_name = "src";
+        let dst_name = "dst";
+
+        let create_msg = Create {
+            parent: root,
+            name: src_name,
+            flags: Flags::default(),
+            mode: libc::S_IFREG | 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, .. } = bt.create(from, create_msg).await.unwrap();
+        let link_msg = Link {
+            inode,
+            new_parent: root,
+            name: dst_name,
+        };
+        bt.link(from, link_msg).await.unwrap();
+        let unlink_msg = Unlink {
+            parent: root,
+            name: src_name,
+        };
+        bt.unlink(from, unlink_msg).await.unwrap();
+
+        let lookup_msg = Lookup {
+            parent: root,
+            name: dst_name,
+        };
+        let LookupReply {
+            inode: actual_inode,
+            ..
+        } = bt.lookup(from, lookup_msg).await.unwrap();
+        assert_eq!(inode, actual_inode);
+        let lookup_msg = Lookup {
+            parent: root,
+            name: src_name,
+        };
+        let result = bt.lookup(from, lookup_msg).await;
+        assert_enoent(result);
+    }
+
+    #[tokio::test]
+    async fn rename_to_different_directory() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+        let root = SpecInodes::RootDir.into();
+        let dir_name = "dir";
+        let file_name = "file";
+
+        let create_msg = Create {
+            parent: root,
+            name: dir_name,
+            flags: libc::O_DIRECTORY.into(),
+            mode: libc::S_IFDIR | 0o755,
+            umask: 0,
+        };
+        let CreateReply { inode: dir, .. } = bt.create(from, create_msg).await.unwrap();
+        let create_msg = Create {
+            parent: root,
+            name: file_name,
+            flags: Flags::default(),
+            mode: libc::S_IFREG | 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode: file, .. } = bt.create(from, create_msg).await.unwrap();
+        let link_msg = Link {
+            inode: file,
+            new_parent: dir,
+            name: file_name,
+        };
+        bt.link(from, link_msg).await.unwrap();
+        let unlink_msg = Unlink {
+            parent: root,
+            name: file_name,
+        };
+        bt.unlink(from, unlink_msg).await.unwrap();
+
+        let lookup_msg = Lookup {
+            parent: dir,
+            name: file_name,
+        };
+        let LookupReply {
+            inode: actual_inode,
+            ..
+        } = bt.lookup(from, lookup_msg).await.unwrap();
+        assert_eq!(file, actual_inode);
+        let lookup_msg = Lookup {
+            parent: root,
+            name: file_name,
+        };
+        let result = bt.lookup(from, lookup_msg).await;
+        assert_enoent(result);
+    }
+
+    #[tokio::test]
+    async fn rename_no_replace_same_directory() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+        let root: Inode = SpecInodes::RootDir.into();
+        let oldname = "old";
+        let newname = "new";
+
+        let create_msg = Create {
+            parent: root,
+            name: oldname,
+            flags: Flags::default(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, .. } = bt.create(from, create_msg).await.unwrap();
+        let create_msg = Create {
+            parent: root,
+            name: newname,
+            flags: Flags::default(),
+            mode: 0o644,
+            umask: 0,
+        };
+        bt.create(from, create_msg).await.unwrap();
+
+        let link_msg = Link {
+            inode,
+            new_parent: root,
+            name: newname,
+        };
+        let result = bt.link(from, link_msg).await;
+        let err = result.err().unwrap().downcast::<io::Error>().unwrap();
+        assert_eq!(io::ErrorKind::AlreadyExists, err.kind());
+    }
+
+    #[tokio::test]
+    async fn rename_no_replace_different_directory() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let from = case.from();
+        let root = SpecInodes::RootDir.into();
+        let dir_name = "dir";
+        let file_name = "file";
+
+        let create_msg = Create {
+            parent: root,
+            name: dir_name,
+            flags: libc::O_DIRECTORY.into(),
+            mode: 0o755,
+            umask: 0,
+        };
+        let CreateReply { inode: dir, .. } = bt.create(from, create_msg).await.unwrap();
+        let create_msg = Create {
+            parent: root,
+            name: file_name,
+            flags: Flags::default(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, .. } = bt.create(from, create_msg).await.unwrap();
+        let create_msg = Create {
+            parent: dir,
+            name: file_name,
+            flags: Flags::default(),
+            mode: 0o644,
+            umask: 0,
+        };
+        bt.create(from, create_msg).await.unwrap();
+
+        let link_msg = Link {
+            inode,
+            new_parent: dir,
+            name: file_name,
+        };
+        let result = bt.link(from, link_msg).await;
+        let err = result.err().unwrap().downcast::<io::Error>().unwrap();
+        assert_eq!(io::ErrorKind::AlreadyExists, err.kind());
+    }
+
+    #[tokio::test]
+    async fn read_from_non_owner_is_err() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let name = "file.txt";
+        let owner = case.from();
+        let mut other = owner.as_ref().clone();
+        other.push_component("subdir".to_owned());
+        let other = Arc::new(other);
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: libc::O_RDWR.into(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(owner, create_msg).await.unwrap();
+        let result = bt
+            .write(&other, inode, handle, 0, 3, [1, 2, 3].as_slice())
+            .await;
+
+        let err = result.err().unwrap().downcast::<Error>().unwrap();
+        let matched = if let Error::WrongOwner = err {
+            true
+        } else {
+            false
+        };
+        assert!(matched)
+    }
+
+    #[tokio::test]
+    async fn allocate_full_sectors_zero_remain_non_zero() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let name = "file.txt";
+        let from = case.from();
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: libc::O_RDWR.into(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+        let mut actual = [0u8; 8];
+        let alloc_msg = Allocate {
+            inode,
+            handle,
+            offset: 0,
+            size: actual.len() as u64,
+        };
+        bt.allocate(from, alloc_msg).await.unwrap();
+        let read_msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: actual.len() as u64,
+        };
+        bt.read(from, read_msg, |data| actual.copy_from_slice(data))
+            .unwrap();
+        let read_meta_msg = ReadMeta {
+            inode,
+            handle: Some(handle),
+        };
+        let ReadMetaReply { meta, .. } = bt.read_meta(from, read_meta_msg).await.unwrap();
+
+        assert_eq!([0u8; 8], actual);
+        assert_eq!(actual.len() as u64, meta.body().secrets().unwrap().size);
+    }
+
+    #[tokio::test]
+    async fn allocate_full_sectors_non_zero_remain_non_zero() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let name = "file.txt";
+        let from = case.from();
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: libc::O_RDWR.into(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+        const LEN: usize = SECTOR_SZ_DEFAULT + 1;
+        let mut size = LEN as u64;
+        let alloc_msg = Allocate {
+            inode,
+            handle,
+            offset: 0,
+            size,
+        };
+        bt.allocate(from, alloc_msg).await.unwrap();
+        let mut actual = Cursor::new(Vec::with_capacity(LEN));
+        while size > 0 {
+            let read_msg = Read {
+                inode,
+                handle,
+                offset: 0,
+                size,
+            };
+            let cb = |data: &[u8]| {
+                actual.write(data)?;
+                size -= data.len() as u64;
+                Ok::<_, io::Error>(())
+            };
+            bt.read(from, read_msg, cb).unwrap().unwrap();
+        }
+        let read_meta_msg = ReadMeta {
+            inode,
+            handle: Some(handle),
+        };
+        let ReadMetaReply { meta, .. } = bt.read_meta(from, read_meta_msg).await.unwrap();
+
+        assert_eq!(vec![0u8; LEN], actual.into_inner());
+        assert_eq!(LEN as u64, meta.body().secrets().unwrap().size);
+    }
+
+    #[tokio::test]
+    async fn allocate_full_sectors_non_zero_remain_zero() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let name = "file.txt";
+        let from = case.from();
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: libc::O_RDWR.into(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+        const LEN: usize = SECTOR_SZ_DEFAULT;
+        let size = LEN as u64;
+        let alloc_msg = Allocate {
+            inode,
+            handle,
+            offset: 0,
+            size,
+        };
+        bt.allocate(from, alloc_msg).await.unwrap();
+        let mut actual = vec![0u8; LEN];
+        let read_msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size,
+        };
+        bt.read(from, read_msg, |data| actual.copy_from_slice(data))
+            .unwrap();
+        let read_meta_msg = ReadMeta {
+            inode,
+            handle: Some(handle),
+        };
+        let ReadMetaReply { meta, .. } = bt.read_meta(from, read_meta_msg).await.unwrap();
+
+        assert_eq!(vec![0u8; LEN], actual);
+        assert_eq!(LEN as u64, meta.body().secrets().unwrap().size);
+    }
+
+    /// Tests that when the new_size of the block is not greater than the current size of the block,
+    /// then no change to the block occurs.
+    #[tokio::test]
+    async fn allocate_new_size_not_greater_than_curr_size() {
+        let case = LocalFsTest::new_empty();
+        let bt = &case.fs;
+        let name = "file.txt";
+        let from = case.from();
+
+        let create_msg = Create {
+            parent: SpecInodes::RootDir.into(),
+            name,
+            flags: libc::O_RDWR.into(),
+            mode: 0o644,
+            umask: 0,
+        };
+        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
+        const LEN: usize = 8;
+        let WriteReply { written, .. } = bt
+            .write(from, inode, handle, 0, LEN as u64, [1u8; LEN].as_slice())
+            .await
+            .unwrap();
+        assert_eq!(8, written);
+        let alloc_msg = Allocate {
+            inode,
+            handle,
+            offset: 0,
+            size: (LEN / 2) as u64,
+        };
+        bt.allocate(from, alloc_msg).await.unwrap();
+        let mut actual = [0u8; LEN];
+        let read_msg = Read {
+            inode,
+            handle,
+            offset: 0,
+            size: LEN as u64,
+        };
+        bt.read(from, read_msg, |data| actual.copy_from_slice(data))
+            .unwrap();
+        let read_meta_msg = ReadMeta {
+            inode,
+            handle: Some(handle),
+        };
+        let ReadMetaReply { meta, .. } = bt.read_meta(from, read_meta_msg).await.unwrap();
+
+        assert_eq!([1u8; LEN], actual);
+        assert_eq!(LEN as u64, meta.body().secrets().unwrap().size);
+    }
+}

+ 171 - 0
crates/btfproto-tests/src/mode_authorizer.rs

@@ -0,0 +1,171 @@
+use super::node_creds;
+
+use btfproto::local_fs::AuthzContext;
+use btlib::BlockMeta;
+
+pub struct TestCase {
+    ctx_uid: u32,
+    ctx_gid: u32,
+    meta: BlockMeta,
+}
+
+impl TestCase {
+    pub const BLOCK_UID: u32 = 1000;
+    pub const BLOCK_GID: u32 = 1000;
+
+    pub fn new(ctx_uid: u32, ctx_gid: u32, mode: u32) -> TestCase {
+        let mut meta = BlockMeta::new(node_creds()).expect("failed to create block metadata");
+        meta.mut_body()
+            .access_secrets(|secrets| {
+                secrets.uid = Self::BLOCK_UID;
+                secrets.gid = Self::BLOCK_GID;
+                secrets.mode = mode;
+                Ok(())
+            })
+            .expect("failed to update secrets");
+        TestCase {
+            ctx_uid,
+            ctx_gid,
+            meta,
+        }
+    }
+
+    pub fn context(&self) -> AuthzContext<'_> {
+        AuthzContext {
+            uid: self.ctx_uid,
+            gid: self.ctx_gid,
+            meta: &self.meta,
+        }
+    }
+}
+
+#[cfg(test)]
+/// Tests for the [ModeAuthorizer] struct.
+mod tests {
+    use super::*;
+
+    use btfproto::local_fs::{Authorizer, ModeAuthorizer};
+
+    #[test]
+    fn cant_read_when_no_bits_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, 0);
+        let result = ModeAuthorizer {}.can_read(&case.context());
+        assert!(result.is_err())
+    }
+
+    #[test]
+    fn cant_write_when_no_bits_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, 0);
+        let result = ModeAuthorizer {}.can_write(&case.context());
+        assert!(result.is_err())
+    }
+
+    #[test]
+    fn cant_exec_when_no_bits_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, 0);
+        let result = ModeAuthorizer {}.can_exec(&case.context());
+        assert!(result.is_err())
+    }
+
+    #[test]
+    fn user_can_read_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IRUSR);
+        let result = ModeAuthorizer {}.can_read(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn user_can_write_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IWUSR);
+        let result = ModeAuthorizer {}.can_write(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn user_can_exec_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IXUSR);
+        let result = ModeAuthorizer {}.can_exec(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn group_can_read_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IRGRP);
+        let result = ModeAuthorizer {}.can_read(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn group_can_write_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IWGRP);
+        let result = ModeAuthorizer {}.can_write(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn group_can_exec_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IXGRP);
+        let result = ModeAuthorizer {}.can_exec(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn other_can_read_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IROTH);
+        let result = ModeAuthorizer {}.can_read(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn other_can_write_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IWOTH);
+        let result = ModeAuthorizer {}.can_write(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn other_can_exec_when_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IXOTH);
+        let result = ModeAuthorizer {}.can_exec(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn other_cant_write_even_if_user_can() {
+        let case = TestCase::new(
+            TestCase::BLOCK_UID + 1,
+            TestCase::BLOCK_GID + 1,
+            libc::S_IWUSR,
+        );
+        let result = ModeAuthorizer {}.can_write(&case.context());
+        assert!(result.is_err())
+    }
+
+    #[test]
+    fn other_cant_write_even_if_group_can() {
+        let case = TestCase::new(
+            TestCase::BLOCK_UID + 1,
+            TestCase::BLOCK_GID + 1,
+            libc::S_IWGRP,
+        );
+        let result = ModeAuthorizer {}.can_write(&case.context());
+        assert!(result.is_err())
+    }
+
+    #[test]
+    fn user_allowed_read_when_only_other_bit_set() {
+        let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IROTH);
+        let result = ModeAuthorizer {}.can_read(&case.context());
+        assert!(result.is_ok())
+    }
+
+    #[test]
+    fn root_always_allowed() {
+        let case = TestCase::new(0, 0, 0);
+        let ctx = case.context();
+        let authorizer = ModeAuthorizer {};
+        assert!(authorizer.can_read(&ctx).is_ok());
+        assert!(authorizer.can_write(&ctx).is_ok());
+        assert!(authorizer.can_exec(&ctx).is_ok());
+    }
+}

+ 0 - 4
crates/btfproto/Cargo.toml

@@ -24,7 +24,3 @@ positioned-io = { version = "0.3.1", optional = true }
 fuse-backend-rs = { version = "0.9.6", optional = true }
 btserde = { path = "../btserde", optional = true }
 bytes = { version = "1.3.0", optional = true }
-
-[dev-dependencies]
-tempdir = { version = "0.3.7" }
-lazy_static = { version = "1.4.0" }

+ 8 - 6
crates/btfproto/src/client.rs

@@ -1,6 +1,6 @@
-use crate::msg::*;
+use crate::{msg::*, Handle, Inode};
 
-use btlib::{bterr, BlockMeta, Result};
+use btlib::{bterr, Result};
 use btmsg::{DeserCallback, Transmitter};
 
 use core::future::{ready, Future, Ready};
@@ -70,7 +70,7 @@ struct ExtractReadMeta;
 
 impl DeserCallback for ExtractReadMeta {
     type Arg<'de> = FsReply<'de>;
-    type Return = Result<BlockMeta>;
+    type Return = Result<ReadMetaReply>;
     type CallFut<'de> = Ready<Self::Return>;
     fn call<'de>(&'de mut self, arg: Self::Arg<'de>) -> Self::CallFut<'de> {
         let result = if let FsReply::ReadMeta(value) = arg {
@@ -210,7 +210,7 @@ impl<T: Transmitter> FsClient<T> {
         self.tx.call(msg, AckCallback).await?
     }
 
-    pub async fn read_meta(&self, inode: Inode, handle: Option<Handle>) -> Result<BlockMeta> {
+    pub async fn read_meta(&self, inode: Inode, handle: Option<Handle>) -> Result<ReadMetaReply> {
         let msg = FsMsg::ReadMeta(ReadMeta { inode, handle });
         self.tx.call(msg, ExtractReadMeta).await?
     }
@@ -219,12 +219,14 @@ impl<T: Transmitter> FsClient<T> {
         &self,
         inode: Inode,
         handle: Option<Handle>,
-        meta: BlockMeta,
+        attrs: Attrs,
+        attrs_set: AttrsSet,
     ) -> Result<()> {
         let msg = FsMsg::WriteMeta(WriteMeta {
             inode,
             handle,
-            meta,
+            attrs,
+            attrs_set,
         });
         self.tx.call(msg, AckCallback).await?
     }

+ 4 - 1
crates/btfproto/src/lib.rs

@@ -1,6 +1,9 @@
 #![feature(type_alias_impl_trait)]
 
-mod msg;
+pub type Inode = u64;
+pub type Handle = u64;
+
+pub mod msg;
 
 #[cfg(feature = "client")]
 pub mod client;

+ 125 - 894
crates/btfproto/src/local_fs.rs

@@ -1,5 +1,15 @@
 use crate::{msg::*, server::FsProvider};
 
+use btlib::{
+    accessor::Accessor,
+    bterr,
+    crypto::{Creds, Decrypter, Signer},
+    error::{BtErr, DisplayErr},
+    BlockAccessor, BlockError, BlockMeta, BlockMetaSecrets, BlockOpenOptions, BlockPath,
+    BlockReader, BlockRecord, DirEntry, DirEntryKind, Directory, Epoch, FileBlock, FlushMeta,
+    MetaAccess, MetaReader, Positioned, Principaled, Result, Split, TrySeek, WriteDual,
+    ZeroExtendable,
+};
 use btserde::{read_from, write_to};
 use core::future::{ready, Ready};
 use log::{debug, error, warn};
@@ -9,7 +19,7 @@ use std::{
     collections::hash_map::{self, HashMap},
     fmt::{Display, Formatter},
     fs::File,
-    io::{self, Seek, SeekFrom, Write as IoWrite},
+    io::{self, Read as IoRead, Seek, SeekFrom, Write as IoWrite},
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicU64, Ordering},
@@ -18,17 +28,7 @@ use std::{
     time::Duration,
 };
 
-use btlib::{
-    accessor::Accessor,
-    bterr,
-    crypto::{Creds, Decrypter, Signer},
-    error::{BtErr, DisplayErr},
-    BlockAccessor, BlockError, BlockMeta, BlockMetaSecrets, BlockOpenOptions, BlockPath,
-    BlockReader, BlockRecord, DirEntry, DirEntryKind, Directory, Epoch, FileBlock, FlushMeta,
-    MetaAccess, MetaReader, Positioned, Principaled, Result, Split, TrySeek,
-};
-
-pub use private::{LocalFsProvider, ModeAuthorizer, SpecInodes};
+pub use private::{Authorizer, AuthzContext, Error, LocalFs, ModeAuthorizer};
 
 mod private {
     use super::*;
@@ -81,19 +81,6 @@ mod private {
 
     impl std::error::Error for Error {}
 
-    #[repr(u64)]
-    pub enum SpecInodes {
-        RootDir = 1,
-        Sb = 2,
-        FirstFree = 11,
-    }
-
-    impl From<SpecInodes> for Inode {
-        fn from(special: SpecInodes) -> Self {
-            special as Inode
-        }
-    }
-
     #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
     #[repr(u32)] // This type needs to match `libc::mode_t`.
     /// The type of a file (regular, directory, etc).
@@ -533,7 +520,7 @@ mod private {
     }
 
     /// Structure for managing the part of a blocktree which is stored in the local filesystem.
-    pub struct LocalFsProvider<C, A> {
+    pub struct LocalFs<C, A> {
         /// The path to the directory in the local filesystem where this blocktree is located.
         path: PathBuf,
         /// A map from inode numbers to their reference counts.
@@ -548,20 +535,20 @@ mod private {
         authorizer: A,
     }
 
-    impl<C, A> LocalFsProvider<C, A> {
+    impl<C, A> LocalFs<C, A> {
         /// The maximum number of directory entries that can be returned in any given call to
         /// `read_dir`.
         const DIR_ENTRY_LIMIT: usize = 1024;
     }
 
-    impl<C: Creds + 'static, A> LocalFsProvider<C, A> {
+    impl<C: Creds + 'static, A> LocalFs<C, A> {
         /// Creates a new empty blocktree at the given path.
         pub fn new_empty(
             btdir: PathBuf,
             generation: u64,
             creds: C,
             authorizer: A,
-        ) -> Result<LocalFsProvider<C, A>> {
+        ) -> Result<LocalFs<C, A>> {
             let root_block_path = creds
                 .writecap()
                 .ok_or(BlockError::MissingWritecap)?
@@ -613,11 +600,7 @@ mod private {
         }
 
         /// Opens an existing blocktree stored at the given path.
-        pub fn new_existing(
-            btdir: PathBuf,
-            creds: C,
-            authorizer: A,
-        ) -> Result<LocalFsProvider<C, A>> {
+        pub fn new_existing(btdir: PathBuf, creds: C, authorizer: A) -> Result<LocalFs<C, A>> {
             let root_block_path = creds
                 .writecap()
                 .ok_or(BlockError::MissingWritecap)?
@@ -645,7 +628,7 @@ mod private {
             root_block: Accessor<FileBlock<C>>,
             creds: C,
             authorizer: A,
-        ) -> Result<LocalFsProvider<C, A>> {
+        ) -> Result<LocalFs<C, A>> {
             let mut inodes = HashMap::with_capacity(1);
             let empty_path = Arc::new(BlockPath::default());
             inodes.insert(
@@ -656,7 +639,7 @@ mod private {
                 SpecInodes::RootDir.into(),
                 RwLock::new(InodeTableValue::new(root_block, empty_path)),
             );
-            Ok(LocalFsProvider {
+            Ok(LocalFs {
                 path: btdir,
                 inodes: RwLock::new(inodes),
                 next_inode: AtomicU64::new(sb.next_inode),
@@ -923,16 +906,16 @@ mod private {
         }
     }
 
-    unsafe impl<C: Sync, A: Sync> Sync for LocalFsProvider<C, A> {}
+    unsafe impl<C: Sync, A: Sync> Sync for LocalFs<C, A> {}
 
-    impl<C: 'static + Creds + Clone + Sync, A: 'static + Authorizer + Sync> FsProvider
-        for LocalFsProvider<C, A>
+    impl<C: 'static + Creds + Clone + Send + Sync, A: 'static + Authorizer + Send + Sync> FsProvider
+        for LocalFs<C, A>
     {
         type LookupFut<'c> = Ready<Result<LookupReply>>;
         fn lookup<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Lookup<'c>) -> Self::LookupFut<'c> {
             let result = (move || {
                 let Lookup { parent, name, .. } = msg;
-                debug!("LocalFsProvider::lookup, parent {parent}, {:?}", name);
+                debug!("lookup: parent {parent}, {:?}", name);
                 let (dir, block_path) = match self.borrow_block_mut(parent, |parent_block| {
                     self.authorizer
                         .can_exec(&self.authz_context(from, parent_block.meta()))?;
@@ -942,7 +925,7 @@ mod private {
                 }) {
                     Ok(pair) => pair,
                     Err(err) => {
-                        error!("LocalFsProvider::lookup failed to borrow inode {parent}: {err}");
+                        error!("lookup failed to borrow inode {parent}: {err}");
                         return Err(err);
                     }
                 };
@@ -959,7 +942,7 @@ mod private {
                 }) {
                     Ok(stat) => stat,
                     Err(err) => {
-                        error!("LocalFsProvider::lookup failed to read stats for '{name}': {err}");
+                        error!("lookup failed to read stats for '{name}': {err}");
                         return Err(err);
                     }
                 };
@@ -984,7 +967,7 @@ mod private {
                     mode,
                     umask,
                 } = msg;
-                debug!("Blocktree::create, parent {parent}, name {:?}", name);
+                debug!("create: parent {parent}, name {:?}", name);
 
                 let name = msg.name.to_owned();
 
@@ -1059,7 +1042,7 @@ mod private {
         fn open<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Open) -> Self::OpenFut<'c> {
             let result = (move || {
                 let Open { inode, flags } = msg;
-                debug!("Blocktree::open, inode {inode}, flags {flags}");
+                debug!("open: inode {inode}, flags {flags}");
                 if flags.value() & libc::O_APPEND != 0 {
                     return Err(Self::unsupported_flag_err("O_APPEND"));
                 }
@@ -1091,7 +1074,7 @@ mod private {
 
         fn read<'c, R, F>(&'c self, from: &'c Arc<BlockPath>, msg: Read, callback: F) -> Result<R>
         where
-            F: 'c + Send + FnOnce(&[u8]) -> R,
+            F: 'c + FnOnce(&[u8]) -> R,
         {
             let Read {
                 inode,
@@ -1099,7 +1082,7 @@ mod private {
                 offset,
                 size,
             } = msg;
-            debug!("inode {inode}, handle {handle}, offset {offset}, size {size}");
+            debug!("read: inode {inode}, handle {handle}, offset {offset}, size {size}");
             let mut callback = Some(callback);
             let output = self.access_block(from, inode, handle, |block, flags| {
                 flags.assert_readable()?;
@@ -1138,22 +1121,28 @@ mod private {
         }
 
         type WriteFut<'c> = Ready<Result<WriteReply>>;
-        fn write<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Write) -> Self::WriteFut<'c> {
+        fn write<'c, R>(
+            &'c self,
+            from: &'c Arc<BlockPath>,
+            inode: Inode,
+            handle: Handle,
+            offset: u64,
+            size: u64,
+            reader: R,
+        ) -> Self::WriteFut<'c>
+        where
+            R: 'c + IoRead,
+        {
             let result = (move || {
-                let Write {
-                    inode,
-                    handle,
-                    offset,
-                    data,
-                } = msg;
-                debug!("inode {inode}, handle {handle}, offset {offset}");
+                debug!("write: inode {inode}, handle {handle}, offset {offset}");
                 let written = self.access_block_mut(from, inode, handle, |block, flags| {
                     flags.assert_writeable()?;
                     let pos = block.pos() as u64;
                     if offset != pos {
                         block.seek(SeekFrom::Start(offset))?;
                     }
-                    block.write(data).bterr()
+                    let size = size.try_into().display_err()?;
+                    block.write_from(reader, size).bterr()
                 })?;
                 Ok(WriteReply {
                     written: written as u64,
@@ -1166,7 +1155,7 @@ mod private {
         fn flush<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Flush) -> Self::FlushFut<'c> {
             let result = (|| {
                 let Flush { inode, handle } = msg;
-                debug!("inode {inode}, handle {handle}");
+                debug!("flush: inode {inode}, handle {handle}");
                 self.access_block_mut(from, inode, handle, |block, flags| {
                     flags.assert_writeable()?;
                     block.flush().bterr()
@@ -1184,7 +1173,7 @@ mod private {
                     limit,
                     state,
                 } = msg;
-                debug!("inode {inode}, handle {handle}, state {state}");
+                debug!("read_dir: inode {inode}, handle {handle}, state {state}");
                 self.access_value(inode, |value| {
                     let handle_value = value
                         .value(handle)
@@ -1217,14 +1206,14 @@ mod private {
             ready(result)
         }
 
-        type LinkFut<'c> = Ready<Result<()>>;
+        type LinkFut<'c> = Ready<Result<LinkReply>>;
         fn link<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Link) -> Self::LinkFut<'c> {
             let Link {
                 inode,
                 new_parent,
                 name,
             } = msg;
-            debug!("inode {inode}, new_parent {new_parent}, name {name}");
+            debug!("link: inode {inode}, new_parent {new_parent}, name {name}");
             let result = self.borrow_block_mut(new_parent, |parent_block| {
                 self.authorizer
                     .can_write(&self.authz_context(from, parent_block.meta()))?;
@@ -1234,25 +1223,26 @@ mod private {
                     return Err(io::Error::from_raw_os_error(libc::EEXIST).into());
                 }
 
-                let file_type = self.access_value_mut(inode, |value| {
+                let attr = self.access_value_mut(inode, |value| {
                     let block = value.block_mut();
                     let meta = block.mut_meta_body();
-                    let mode = meta.access_secrets(|secrets| {
+                    let attr = meta.access_secrets(|secrets| {
                         secrets.nlink += 1;
-                        Ok(secrets.mode)
+                        Ok(secrets.to_owned())
                     })?;
-                    let file_type = FileType::from_value(mode)?;
                     block.flush_meta()?;
                     value.incr_lookup_count(from);
-                    Ok(file_type)
+                    Ok(attr)
                 })?;
+                let file_type = FileType::from_value(attr.mode)?;
                 let entry = match file_type {
                     FileType::Reg => DirEntry::File(BlockRecord::new(inode)),
                     FileType::Dir => DirEntry::Directory(BlockRecord::new(inode)),
                 };
                 dir.insert_entry(name.to_owned(), entry);
                 parent_block.write_dir(&dir)?;
-                Ok(())
+                let entry = self.bt_entry(attr);
+                Ok(LinkReply { entry })
             });
             ready(result)
         }
@@ -1261,7 +1251,7 @@ mod private {
         fn unlink<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Unlink) -> Self::UnlinkFut<'c> {
             let result = (move || {
                 let Unlink { parent, name } = msg;
-                debug!("parent {parent}, name {name}");
+                debug!("unlink: parent {parent}, name {name}");
                 let (block_path, inode) = self.borrow_block_mut(parent, |parent_block| {
                     self.authorizer
                         .can_write(&self.authz_context(from, parent_block.meta()))?;
@@ -1298,7 +1288,7 @@ mod private {
             ready(result)
         }
 
-        type ReadMetaFut<'c> = Ready<Result<BlockMeta>>;
+        type ReadMetaFut<'c> = Ready<Result<ReadMetaReply>>;
         fn read_meta<'c>(
             &'c self,
             from: &'c Arc<BlockPath>,
@@ -1306,18 +1296,23 @@ mod private {
         ) -> Self::ReadMetaFut<'c> {
             let result = (move || {
                 let ReadMeta { inode, handle } = msg;
-                debug!("inode {inode}, handle {:?}", handle);
+                debug!("read_meta: inode {inode}, handle {:?}", handle);
                 let meta = if let Some(handle) = handle {
                     self.access_block(from, inode, handle, |block, _| Ok(block.meta().to_owned()))?
                 } else {
                     self.borrow_block(inode, |block| Ok(block.meta().to_owned()))?
                 };
-                Ok(meta)
+                debug!("read_meta: {:?}", meta.body().secrets()?);
+                let reply = ReadMetaReply {
+                    meta,
+                    valid_for: self.attr_timeout(),
+                };
+                Ok(reply)
             })();
             ready(result)
         }
 
-        type WriteMetaFut<'c> = Ready<Result<()>>;
+        type WriteMetaFut<'c> = Ready<Result<WriteMetaReply>>;
         fn write_meta<'c>(
             &'c self,
             from: &'c Arc<BlockPath>,
@@ -1327,32 +1322,84 @@ mod private {
                 let WriteMeta {
                     inode,
                     handle,
-                    meta,
+                    attrs,
+                    attrs_set,
                 } = msg;
-                debug!("inode {inode}, handle {:?}", handle);
+                debug!("write_meta: inode {inode}, handle {:?}", handle);
                 let cb = |block: &mut FileBlock<C>| {
                     self.authorizer
                         .can_write(&self.authz_context(from, block.meta()))?;
-                    *block.mut_meta() = meta;
+                    let attrs = block.mut_meta_body().access_secrets(|secrets| {
+                        if attrs_set.mode() {
+                            secrets.mode = attrs.mode;
+                        }
+                        if attrs_set.uid() {
+                            secrets.uid = attrs.uid;
+                        }
+                        if attrs_set.gid() {
+                            secrets.gid = attrs.gid;
+                        }
+                        if attrs_set.atime() {
+                            secrets.atime = attrs.atime;
+                        }
+                        if attrs_set.mtime() {
+                            secrets.mtime = attrs.mtime;
+                        }
+                        if attrs_set.ctime() {
+                            secrets.ctime = attrs.ctime;
+                        }
+                        for (key, value) in attrs.tags.into_iter() {
+                            secrets.tags.insert(key, value);
+                        }
+                        Ok(secrets.to_owned())
+                    })?;
                     block.flush_meta()?;
-                    Ok(())
+                    Ok(attrs)
                 };
-                if let Some(handle) = handle {
+                let attrs = if let Some(handle) = handle {
                     self.access_block_mut(from, inode, handle, |block, flags| {
                         flags.assert_writeable()?;
                         cb(block.get_mut())
                     })
                 } else {
                     self.borrow_block_mut(inode, |block| cb(block.get_mut()))
-                }
+                }?;
+                Ok(WriteMetaReply {
+                    attrs,
+                    valid_for: self.attr_timeout(),
+                })
             })();
             ready(result)
         }
 
+        type AllocateFut<'c> = Ready<Result<()>>;
+        fn allocate<'c>(
+            &'c self,
+            from: &'c Arc<BlockPath>,
+            msg: Allocate,
+        ) -> Self::AllocateFut<'c> {
+            let Allocate {
+                inode,
+                handle,
+                offset,
+                size,
+            } = msg;
+            debug!("allocate: inode {inode}, handle {handle}, offset {offset}, size {size}");
+            let result = self.access_block_mut(from, inode, handle, |block, _| {
+                let curr_size = block.meta_body().secrets()?.size;
+                let new_size = curr_size.max(offset + size);
+                if new_size > curr_size {
+                    block.zero_extend(new_size - curr_size)?;
+                }
+                Ok(())
+            });
+            ready(result)
+        }
+
         type CloseFut<'c> = Ready<Result<()>>;
         fn close<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Close) -> Self::CloseFut<'c> {
             let Close { inode, handle } = msg;
-            debug!("inode {inode}, handle {handle}");
+            debug!("close: inode {inode}, handle {handle}");
             let result = self.access_value_mut(inode, |value| {
                 if let Err(err) =
                     value.access_block_mut(from, handle, |block, _| block.flush().bterr())
@@ -1372,7 +1419,7 @@ mod private {
         fn forget<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Forget) -> Self::ForgetFut<'c> {
             let result = (move || {
                 let Forget { inode, count } = msg;
-                debug!("inode {inode}, count {count}");
+                debug!("forget: inode {inode}, count {count}");
                 let mut inodes = self.inodes.write().display_err()?;
                 self.inode_forget(&mut inodes, from, inode, count).bterr()
             })();
@@ -1390,819 +1437,3 @@ mod private {
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-
-    use super::private::Error;
-    use super::*;
-
-    use btlib::{
-        crypto::{ConcreteCreds, CredsPriv},
-        BlockMeta,
-    };
-    use bytes::BytesMut;
-    use lazy_static::lazy_static;
-    use std::sync::Arc;
-    use tempdir::TempDir;
-
-    lazy_static! {
-        static ref ROOT_CREDS: ConcreteCreds = ConcreteCreds::generate().unwrap();
-        static ref NODE_CREDS: ConcreteCreds = {
-            let root_creds = &ROOT_CREDS;
-            let mut node_creds = ConcreteCreds::generate().unwrap();
-            let writecap = root_creds
-                .issue_writecap(
-                    node_creds.principal(),
-                    vec![],
-                    Epoch::now() + Duration::from_secs(3600),
-                )
-                .unwrap();
-            node_creds.set_writecap(writecap);
-            node_creds
-        };
-    }
-
-    fn node_creds() -> &'static ConcreteCreds {
-        &NODE_CREDS
-    }
-
-    /// Tests for the [ModeAuthorizer] struct.
-    mod mode_authorizer_tests {
-        use super::{
-            super::private::{Authorizer, AuthzContext},
-            *,
-        };
-
-        struct TestCase {
-            ctx_uid: u32,
-            ctx_gid: u32,
-            meta: BlockMeta,
-        }
-
-        impl TestCase {
-            const BLOCK_UID: u32 = 1000;
-            const BLOCK_GID: u32 = 1000;
-
-            fn new(ctx_uid: u32, ctx_gid: u32, mode: u32) -> TestCase {
-                let mut meta =
-                    BlockMeta::new(node_creds()).expect("failed to create block metadata");
-                meta.mut_body()
-                    .access_secrets(|secrets| {
-                        secrets.uid = Self::BLOCK_UID;
-                        secrets.gid = Self::BLOCK_GID;
-                        secrets.mode = mode;
-                        Ok(())
-                    })
-                    .expect("failed to update secrets");
-                TestCase {
-                    ctx_uid,
-                    ctx_gid,
-                    meta,
-                }
-            }
-
-            fn context(&self) -> AuthzContext<'_> {
-                AuthzContext {
-                    uid: self.ctx_uid,
-                    gid: self.ctx_gid,
-                    meta: &self.meta,
-                }
-            }
-        }
-
-        #[test]
-        fn cant_read_when_no_bits_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, 0);
-            let result = ModeAuthorizer {}.can_read(&case.context());
-            assert!(result.is_err())
-        }
-
-        #[test]
-        fn cant_write_when_no_bits_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, 0);
-            let result = ModeAuthorizer {}.can_write(&case.context());
-            assert!(result.is_err())
-        }
-
-        #[test]
-        fn cant_exec_when_no_bits_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, 0);
-            let result = ModeAuthorizer {}.can_exec(&case.context());
-            assert!(result.is_err())
-        }
-
-        #[test]
-        fn user_can_read_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IRUSR);
-            let result = ModeAuthorizer {}.can_read(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn user_can_write_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IWUSR);
-            let result = ModeAuthorizer {}.can_write(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn user_can_exec_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IXUSR);
-            let result = ModeAuthorizer {}.can_exec(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn group_can_read_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IRGRP);
-            let result = ModeAuthorizer {}.can_read(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn group_can_write_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IWGRP);
-            let result = ModeAuthorizer {}.can_write(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn group_can_exec_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IXGRP);
-            let result = ModeAuthorizer {}.can_exec(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn other_can_read_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IROTH);
-            let result = ModeAuthorizer {}.can_read(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn other_can_write_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IWOTH);
-            let result = ModeAuthorizer {}.can_write(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn other_can_exec_when_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IXOTH);
-            let result = ModeAuthorizer {}.can_exec(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn other_cant_write_even_if_user_can() {
-            let case = TestCase::new(
-                TestCase::BLOCK_UID + 1,
-                TestCase::BLOCK_GID + 1,
-                libc::S_IWUSR,
-            );
-            let result = ModeAuthorizer {}.can_write(&case.context());
-            assert!(result.is_err())
-        }
-
-        #[test]
-        fn other_cant_write_even_if_group_can() {
-            let case = TestCase::new(
-                TestCase::BLOCK_UID + 1,
-                TestCase::BLOCK_GID + 1,
-                libc::S_IWGRP,
-            );
-            let result = ModeAuthorizer {}.can_write(&case.context());
-            assert!(result.is_err())
-        }
-
-        #[test]
-        fn user_allowed_read_when_only_other_bit_set() {
-            let case = TestCase::new(TestCase::BLOCK_UID, TestCase::BLOCK_GID, libc::S_IROTH);
-            let result = ModeAuthorizer {}.can_read(&case.context());
-            assert!(result.is_ok())
-        }
-
-        #[test]
-        fn root_always_allowed() {
-            let case = TestCase::new(0, 0, 0);
-            let ctx = case.context();
-            let authorizer = ModeAuthorizer {};
-            assert!(authorizer.can_read(&ctx).is_ok());
-            assert!(authorizer.can_write(&ctx).is_ok());
-            assert!(authorizer.can_exec(&ctx).is_ok());
-        }
-    }
-
-    fn bind_path<C: CredsPriv>(creds: C) -> BlockPath {
-        let writecap = creds
-            .writecap()
-            .ok_or(btlib::BlockError::MissingWritecap)
-            .unwrap();
-        writecap.bind_path()
-    }
-
-    struct BtTestCase {
-        dir: TempDir,
-        bt: LocalFsProvider<ConcreteCreds, ModeAuthorizer>,
-        from: Arc<BlockPath>,
-    }
-
-    impl BtTestCase {
-        fn new_empty() -> BtTestCase {
-            let dir = TempDir::new("fuse").expect("failed to create temp dir");
-            let bt = LocalFsProvider::new_empty(
-                dir.path().to_owned(),
-                0,
-                Self::creds(),
-                ModeAuthorizer {},
-            )
-            .expect("failed to create empty blocktree");
-            let from = Arc::new(bind_path(node_creds()));
-            BtTestCase { dir, bt, from }
-        }
-
-        fn new_existing(dir: TempDir) -> BtTestCase {
-            let bt = LocalFsProvider::new_existing(
-                dir.path().to_owned(),
-                Self::creds(),
-                ModeAuthorizer {},
-            )
-            .expect("failed to create blocktree from existing directory");
-            let from = Arc::new(bind_path(node_creds()));
-            BtTestCase { dir, bt, from }
-        }
-
-        fn creds() -> ConcreteCreds {
-            node_creds().clone()
-        }
-
-        fn from(&self) -> &Arc<BlockPath> {
-            &self.from
-        }
-    }
-
-    /// Tests that a new file can be created, written to and the written data can be read from it.
-    #[tokio::test]
-    async fn create_write_read() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-        let name = "README.md";
-        let flags = Flags::new(libc::O_RDWR);
-
-        let create_msg = Create {
-            parent: SpecInodes::RootDir.into(),
-            name,
-            flags,
-            mode: libc::S_IFREG | 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
-
-        const LEN: usize = 32;
-        let expected = [1u8; LEN];
-        let write_msg = Write {
-            inode,
-            handle,
-            offset: 0,
-            data: expected.as_slice(),
-        };
-        let WriteReply { written, .. } = bt.write(from, write_msg).await.unwrap();
-        assert_eq!(LEN as u64, written);
-
-        let mut actual = [0u8; LEN];
-        let read_msg = Read {
-            inode,
-            handle,
-            offset: 0,
-            size: LEN as u64,
-        };
-        bt.read(from, read_msg, |data| actual.copy_from_slice(data))
-            .unwrap();
-
-        assert_eq!(expected, actual)
-    }
-
-    #[tokio::test]
-    async fn lookup() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-
-        let name = "README.md";
-        let create_msg = Create {
-            parent: SpecInodes::RootDir.into(),
-            name,
-            flags: Flags::default(),
-            mode: 0,
-            umask: 0,
-        };
-        let create_reply = bt.create(from, create_msg).await.unwrap();
-
-        let lookup_msg = Lookup {
-            parent: SpecInodes::RootDir.into(),
-            name,
-        };
-        let lookup_reply = bt.lookup(from, lookup_msg).await.unwrap();
-
-        assert_eq!(create_reply.inode, lookup_reply.inode);
-    }
-
-    /// Tests that data written by one instance of [Blocktree] can be read by a subsequent
-    /// instance.
-    #[tokio::test]
-    async fn new_existing() {
-        const EXPECTED: &[u8] = b"cool as cucumbers";
-        let name = "RESIGNATION.docx";
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-        let flags = Flags::new(libc::O_RDWR);
-
-        {
-            let create_msg = Create {
-                parent: SpecInodes::RootDir.into(),
-                name,
-                mode: libc::S_IFREG | 0o644,
-                flags,
-                umask: 0,
-            };
-            let CreateReply { handle, inode, .. } = bt.create(from, create_msg).await.unwrap();
-
-            let write_msg = Write {
-                inode,
-                handle,
-                offset: 0,
-                data: EXPECTED,
-            };
-            let WriteReply { written, .. } = bt.write(from, write_msg).await.unwrap();
-            assert_eq!(EXPECTED.len() as u64, written);
-
-            let flush_msg = Flush { inode, handle };
-            bt.flush(from, flush_msg).await.unwrap();
-        }
-
-        let case = BtTestCase::new_existing(case.dir);
-        let from = case.from();
-        let bt = &case.bt;
-
-        let lookup_msg = Lookup {
-            parent: SpecInodes::RootDir.into(),
-            name,
-        };
-        let LookupReply { inode, .. } = bt.lookup(from, lookup_msg).await.unwrap();
-
-        let open_msg = Open {
-            inode,
-            flags: Flags::new(libc::O_RDONLY),
-        };
-        let OpenReply { handle, .. } = bt.open(from, open_msg).await.unwrap();
-
-        let mut actual = BytesMut::new();
-        let read_msg = Read {
-            inode,
-            handle,
-            offset: 0,
-            size: EXPECTED.len() as u64,
-        };
-        bt.read(from, read_msg, |data| actual.extend_from_slice(data))
-            .unwrap();
-
-        assert_eq!(EXPECTED, &actual)
-    }
-
-    /// Tests that an error is returned by the `Blocktree::write` method if the file was opened
-    /// read-only.
-    #[tokio::test]
-    async fn open_read_only_write_is_error() {
-        let name = "books.ods";
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-
-        let create_msg = Create {
-            parent: SpecInodes::RootDir.into(),
-            name,
-            flags: Flags::default(),
-            mode: libc::S_IFREG | 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
-
-        let close_msg = Close { inode, handle };
-        bt.close(from, close_msg).await.unwrap();
-
-        let open_msg = Open {
-            inode,
-            flags: libc::O_RDONLY.into(),
-        };
-        let OpenReply { handle, .. } = bt.open(from, open_msg).await.unwrap();
-
-        let data = [1u8; 32];
-        let write_msg = Write {
-            inode,
-            handle,
-            offset: 0,
-            data: &data,
-        };
-        let result = bt.write(from, write_msg).await;
-
-        let err = result.err().unwrap();
-        let err = err.downcast::<Error>().unwrap();
-        let actual_handle = if let Error::ReadOnlyHandle(actual_handle) = err {
-            Some(actual_handle)
-        } else {
-            None
-        };
-        assert_eq!(Some(handle), actual_handle);
-    }
-
-    /// Asserts that the given [Result] is an [Err] and that it contains an [io::Error] which
-    /// corresponds to the [libc::ENOENT] error code.
-    fn assert_enoent<T>(result: Result<T>) {
-        let err = result.err().unwrap().downcast::<io::Error>().unwrap();
-        assert_eq!(libc::ENOENT, err.raw_os_error().unwrap());
-    }
-
-    /// Tests that multiple handles see consistent metadata associated with a block.
-    #[tokio::test]
-    async fn ensure_metadata_consistency() {
-        let case = BtTestCase::new_empty();
-        let from = case.from();
-        let trash = ".Trash";
-        let file = "file.txt";
-        let bt = &case.bt;
-        let root = SpecInodes::RootDir.into();
-
-        let open_msg = Open {
-            inode: root,
-            flags: libc::O_DIRECTORY.into(),
-        };
-        let OpenReply { handle, .. } = bt.open(from, open_msg).await.unwrap();
-
-        // Because the directory is open, this will cause a new handle for this block to be opened.
-        let lookup_msg = Lookup {
-            parent: root,
-            name: trash,
-        };
-        let result = bt.lookup(from, lookup_msg).await;
-        assert_enoent(result);
-
-        let close_msg = Close {
-            inode: root,
-            handle,
-        };
-        bt.close(from, close_msg).await.unwrap();
-
-        let create_msg = Create {
-            parent: root,
-            name: file,
-            flags: Flags::default(),
-            mode: libc::S_IFREG | 0o644,
-            umask: 0,
-        };
-        bt.create(from, create_msg).await.unwrap();
-
-        let open_msg = Open {
-            inode: root,
-            flags: libc::O_DIRECTORY.into(),
-        };
-        bt.open(from, open_msg).await.unwrap();
-
-        // Since the directory is open, the second handle will be used for this lookup.
-        let lookup_msg = Lookup {
-            parent: root,
-            name: trash,
-        };
-        let result = bt.lookup(from, lookup_msg).await;
-        assert!(result.is_err());
-    }
-
-    /// Tests that the `size` parameter actually limits the number of read bytes.
-    #[tokio::test]
-    async fn read_with_smaller_size() {
-        const DATA: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
-        let case = BtTestCase::new_empty();
-        let from = case.from();
-        let file = "file.txt";
-        let bt = &case.bt;
-        let root: Inode = SpecInodes::RootDir.into();
-
-        let create_msg = Create {
-            parent: root,
-            name: file,
-            flags: libc::O_RDWR.into(),
-            mode: libc::S_IFREG | 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
-        let write_msg = Write {
-            inode,
-            handle,
-            offset: 0,
-            data: DATA.as_slice(),
-        };
-        let WriteReply { written, .. } = bt.write(from, write_msg).await.unwrap();
-        assert_eq!(DATA.len() as u64, written);
-        const SIZE: usize = DATA.len() / 2;
-        let mut actual = Vec::with_capacity(SIZE);
-        let read_msg = Read {
-            inode,
-            handle,
-            offset: 0,
-            size: SIZE as u64,
-        };
-        bt.read(from, read_msg, |data| actual.extend_from_slice(data))
-            .unwrap();
-
-        assert_eq!(&[0, 1, 2, 3], actual.as_slice());
-    }
-
-    /// Returns an integer array starting at the given value and increasing by one for each
-    /// subsequent entry.
-    pub const fn integer_array<const N: usize>(start: u8) -> [u8; N] {
-        let mut array = [0u8; N];
-        let mut k = 0usize;
-        while k < N {
-            array[k] = start.wrapping_add(k as u8);
-            k += 1;
-        }
-        array
-    }
-
-    #[tokio::test]
-    async fn concurrent_reads() {
-        // The size of each of the reads.
-        const SIZE: usize = 4;
-        // The number of concurrent reads.
-        const NREADS: usize = 32;
-        const DATA_LEN: usize = SIZE * NREADS;
-        const DATA: [u8; DATA_LEN] = integer_array::<DATA_LEN>(0);
-        let case = BtTestCase::new_empty();
-        let from = case.from();
-        let file = "file.txt";
-        let bt = &case.bt;
-        let root: Inode = SpecInodes::RootDir.into();
-        let mode = libc::S_IFREG | 0o644;
-
-        let create_msg = Create {
-            parent: root,
-            name: file,
-            flags: libc::O_RDWR.into(),
-            mode,
-            umask: 0,
-        };
-        let CreateReply { inode, handle, .. } = bt.create(from, create_msg).await.unwrap();
-        let write_msg = Write {
-            inode,
-            handle,
-            offset: 0,
-            data: DATA.as_slice(),
-        };
-        let WriteReply { written, .. } = bt.write(from, write_msg).await.unwrap();
-        assert_eq!(DATA.len() as u64, written);
-        let case = Box::new(case);
-        let cb = Arc::new(Box::new(move |offset: usize| {
-            // Notice that we have concurrent reads to different offsets using the same handle.
-            // Without proper synchronization, this shouldn't work.
-            let mut actual = Vec::with_capacity(SIZE);
-            let read_msg = Read {
-                inode,
-                handle,
-                offset: offset as u64,
-                size: SIZE as u64,
-            };
-            case.bt
-                .read(case.from(), read_msg, |data| actual.extend_from_slice(data))
-                .unwrap();
-            let expected = integer_array::<SIZE>(offset as u8);
-            assert_eq!(&expected, actual.as_slice());
-        }));
-
-        let mut handles = Vec::with_capacity(NREADS);
-        for offset in (0..NREADS).map(|e| e * SIZE) {
-            let thread_cb = cb.clone();
-            handles.push(std::thread::spawn(move || thread_cb(offset)));
-        }
-        for handle in handles {
-            handle.join().unwrap();
-        }
-    }
-
-    #[tokio::test]
-    async fn rename_in_same_directory() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-        let root: Inode = SpecInodes::RootDir.into();
-        let src_name = "src";
-        let dst_name = "dst";
-
-        let create_msg = Create {
-            parent: root,
-            name: src_name,
-            flags: Flags::default(),
-            mode: libc::S_IFREG | 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, .. } = bt.create(from, create_msg).await.unwrap();
-        let link_msg = Link {
-            inode,
-            new_parent: root,
-            name: dst_name,
-        };
-        bt.link(from, link_msg).await.unwrap();
-        let unlink_msg = Unlink {
-            parent: root,
-            name: src_name,
-        };
-        bt.unlink(from, unlink_msg).await.unwrap();
-
-        let lookup_msg = Lookup {
-            parent: root,
-            name: dst_name,
-        };
-        let LookupReply {
-            inode: actual_inode,
-            ..
-        } = bt.lookup(from, lookup_msg).await.unwrap();
-        assert_eq!(inode, actual_inode);
-        let lookup_msg = Lookup {
-            parent: root,
-            name: src_name,
-        };
-        let result = bt.lookup(from, lookup_msg).await;
-        assert_enoent(result);
-    }
-
-    #[tokio::test]
-    async fn rename_to_different_directory() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-        let root = SpecInodes::RootDir.into();
-        let dir_name = "dir";
-        let file_name = "file";
-
-        let create_msg = Create {
-            parent: root,
-            name: dir_name,
-            flags: libc::O_DIRECTORY.into(),
-            mode: libc::S_IFDIR | 0o755,
-            umask: 0,
-        };
-        let CreateReply { inode: dir, .. } = bt.create(from, create_msg).await.unwrap();
-        let create_msg = Create {
-            parent: root,
-            name: file_name,
-            flags: Flags::default(),
-            mode: libc::S_IFREG | 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode: file, .. } = bt.create(from, create_msg).await.unwrap();
-        let link_msg = Link {
-            inode: file,
-            new_parent: dir,
-            name: file_name,
-        };
-        bt.link(from, link_msg).await.unwrap();
-        let unlink_msg = Unlink {
-            parent: root,
-            name: file_name,
-        };
-        bt.unlink(from, unlink_msg).await.unwrap();
-
-        let lookup_msg = Lookup {
-            parent: dir,
-            name: file_name,
-        };
-        let LookupReply {
-            inode: actual_inode,
-            ..
-        } = bt.lookup(from, lookup_msg).await.unwrap();
-        assert_eq!(file, actual_inode);
-        let lookup_msg = Lookup {
-            parent: root,
-            name: file_name,
-        };
-        let result = bt.lookup(from, lookup_msg).await;
-        assert_enoent(result);
-    }
-
-    #[tokio::test]
-    async fn rename_no_replace_same_directory() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-        let root: Inode = SpecInodes::RootDir.into();
-        let oldname = "old";
-        let newname = "new";
-
-        let create_msg = Create {
-            parent: root,
-            name: oldname,
-            flags: Flags::default(),
-            mode: 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, .. } = bt.create(from, create_msg).await.unwrap();
-        let create_msg = Create {
-            parent: root,
-            name: newname,
-            flags: Flags::default(),
-            mode: 0o644,
-            umask: 0,
-        };
-        bt.create(from, create_msg).await.unwrap();
-
-        let link_msg = Link {
-            inode,
-            new_parent: root,
-            name: newname,
-        };
-        let result = bt.link(from, link_msg).await;
-        let err = result.err().unwrap().downcast::<io::Error>().unwrap();
-        assert_eq!(io::ErrorKind::AlreadyExists, err.kind());
-    }
-
-    #[tokio::test]
-    async fn rename_no_replace_different_directory() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let from = case.from();
-        let root = SpecInodes::RootDir.into();
-        let dir_name = "dir";
-        let file_name = "file";
-
-        let create_msg = Create {
-            parent: root,
-            name: dir_name,
-            flags: libc::O_DIRECTORY.into(),
-            mode: 0o755,
-            umask: 0,
-        };
-        let CreateReply { inode: dir, .. } = bt.create(from, create_msg).await.unwrap();
-        let create_msg = Create {
-            parent: root,
-            name: file_name,
-            flags: Flags::default(),
-            mode: 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, .. } = bt.create(from, create_msg).await.unwrap();
-        let create_msg = Create {
-            parent: dir,
-            name: file_name,
-            flags: Flags::default(),
-            mode: 0o644,
-            umask: 0,
-        };
-        bt.create(from, create_msg).await.unwrap();
-
-        let link_msg = Link {
-            inode,
-            new_parent: dir,
-            name: file_name,
-        };
-        let result = bt.link(from, link_msg).await;
-        let err = result.err().unwrap().downcast::<io::Error>().unwrap();
-        assert_eq!(io::ErrorKind::AlreadyExists, err.kind());
-    }
-
-    #[tokio::test]
-    async fn read_from_non_owner_is_err() {
-        let case = BtTestCase::new_empty();
-        let bt = &case.bt;
-        let name = "file.txt";
-        let owner = case.from();
-        let mut other = owner.as_ref().clone();
-        other.push_component("subdir".to_owned());
-        let other = Arc::new(other);
-
-        let create_msg = Create {
-            parent: SpecInodes::RootDir.into(),
-            name,
-            flags: libc::O_RDWR.into(),
-            mode: 0o644,
-            umask: 0,
-        };
-        let CreateReply { inode, handle, .. } = bt.create(owner, create_msg).await.unwrap();
-        let write_msg = Write {
-            inode,
-            handle,
-            offset: 0,
-            data: &[1, 2, 3],
-        };
-        let result = bt.write(&other, write_msg).await;
-
-        let err = result.err().unwrap().downcast::<Error>().unwrap();
-        let matched = if let Error::WrongOwner = err {
-            true
-        } else {
-            false
-        };
-        assert!(matched)
-    }
-}

+ 127 - 10
crates/btfproto/src/msg.rs

@@ -1,11 +1,15 @@
-use btlib::{BlockMeta, BlockMetaSecrets, DirEntry};
+use super::{Handle, Inode};
+
+use btlib::{BlockMeta, BlockMetaSecrets, DirEntry, Epoch};
 use btmsg::CallMsg;
 use core::time::Duration;
+use paste::paste;
 use serde::{Deserialize, Serialize};
-use std::{fmt::Display, io};
-
-pub type Inode = u64;
-pub type Handle = u64;
+use std::{
+    fmt::Display,
+    io,
+    ops::{BitOr, BitOrAssign},
+};
 
 #[derive(Serialize, Deserialize)]
 pub enum FsMsg<'a> {
@@ -25,6 +29,7 @@ pub enum FsMsg<'a> {
     Unlink(Unlink<'a>),
     ReadMeta(ReadMeta),
     WriteMeta(WriteMeta),
+    Allocate(Allocate),
     Close(Close),
     Forget(Forget),
     Lock(Lock),
@@ -42,7 +47,8 @@ pub enum FsReply<'a> {
     Write(WriteReply),
     ReadDir(ReadDirReply),
     Link(LinkReply),
-    ReadMeta(BlockMeta),
+    ReadMeta(ReadMetaReply),
+    WriteMeta(WriteMetaReply),
 }
 
 impl<'a> CallMsg<'a> for FsMsg<'a> {
@@ -64,6 +70,20 @@ impl Display for FsError {
 
 impl std::error::Error for FsError {}
 
+#[repr(u64)]
+/// An enumeration of special Inodes.
+pub enum SpecInodes {
+    RootDir = 1,
+    Sb = 2,
+    FirstFree = 11,
+}
+
+impl From<SpecInodes> for Inode {
+    fn from(special: SpecInodes) -> Self {
+        special as Inode
+    }
+}
+
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 /// A wrapper type around [i32] with convenience methods for checking if `libc::O_*`
 /// flags have been set.
@@ -87,9 +107,7 @@ impl Flags {
     }
 
     pub fn readable(self) -> bool {
-        // Be careful; because libc::O_RDONLY is 0 you can't use the '&' operator
-        // to test for it, it has to be an equality check.
-        self.0 == libc::O_RDONLY || (self.0 & libc::O_RDWR) != 0
+        !self.write_only()
     }
 
     pub fn read_only(self) -> bool {
@@ -151,6 +169,83 @@ impl Default for Flags {
     }
 }
 
+#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Hash, Default)]
+pub struct Attrs {
+    pub mode: u32,
+    pub uid: u32,
+    pub gid: u32,
+    pub atime: Epoch,
+    pub mtime: Epoch,
+    pub ctime: Epoch,
+    pub tags: Vec<(String, Vec<u8>)>,
+}
+
+#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Hash)]
+/// A type for indicating which fields in [Attrs] have been set and which should be ignored. This
+/// method was chosen over using [Option] for greater efficiency on the wire.
+pub struct AttrsSet(u16);
+
+macro_rules! field {
+    ($index:expr, $name:ident) => {
+        pub const $name: AttrsSet = AttrsSet::new(1 << $index);
+
+        paste! {
+            pub fn [<$name:lower>](self) -> bool {
+                const MASK: u16 = 1 << $index;
+                self.0 & MASK != 0
+            }
+        }
+    };
+}
+
+impl AttrsSet {
+    field!(0, MODE);
+    field!(1, UID);
+    field!(2, GID);
+    field!(3, ATIME);
+    field!(4, MTIME);
+    field!(5, CTIME);
+
+    pub const fn new(value: u16) -> Self {
+        Self(value)
+    }
+
+    pub const fn none() -> Self {
+        Self(0)
+    }
+
+    pub const fn value(self) -> u16 {
+        self.0
+    }
+}
+
+impl Copy for AttrsSet {}
+
+impl From<u16> for AttrsSet {
+    fn from(value: u16) -> Self {
+        Self::new(value)
+    }
+}
+
+impl From<AttrsSet> for u16 {
+    fn from(attr: AttrsSet) -> Self {
+        attr.value()
+    }
+}
+
+impl BitOr<Self> for AttrsSet {
+    type Output = Self;
+    fn bitor(self, rhs: Self) -> Self::Output {
+        AttrsSet::new(self.value() | rhs.value())
+    }
+}
+
+impl BitOrAssign<Self> for AttrsSet {
+    fn bitor_assign(&mut self, rhs: Self) {
+        self.0 |= rhs.0
+    }
+}
+
 #[derive(Serialize, Deserialize)]
 pub struct Entry {
     pub attr: BlockMetaSecrets,
@@ -274,11 +369,33 @@ pub struct ReadMeta {
     pub handle: Option<Handle>,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct ReadMetaReply {
+    pub meta: BlockMeta,
+    pub valid_for: Duration,
+}
+
 #[derive(Serialize, Deserialize)]
 pub struct WriteMeta {
     pub inode: Inode,
     pub handle: Option<Handle>,
-    pub meta: BlockMeta,
+    pub attrs: Attrs,
+    /// The bits in this value determine which fields in `attrs` have been initialized.
+    pub attrs_set: AttrsSet,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct WriteMetaReply {
+    pub attrs: BlockMetaSecrets,
+    pub valid_for: Duration,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Allocate {
+    pub inode: Inode,
+    pub handle: Handle,
+    pub offset: u64,
+    pub size: u64,
 }
 
 #[derive(Serialize, Deserialize)]

+ 142 - 13
crates/btfproto/src/server.rs

@@ -1,12 +1,15 @@
-use crate::msg::*;
+use crate::{
+    msg::{Read as ReadMsg, *},
+    Handle, Inode,
+};
 
-use btlib::{crypto::Creds, BlockMeta, BlockPath, Result};
+use btlib::{crypto::Creds, BlockPath, Result};
 use btmsg::{receiver, MsgCallback, MsgReceived, Receiver};
 use core::future::Future;
-use std::{net::IpAddr, sync::Arc};
+use std::{io::Read, net::IpAddr, sync::Arc};
 use tokio::runtime::Handle as RuntimeHandle;
 
-pub trait FsProvider {
+pub trait FsProvider: Send + Sync {
     type LookupFut<'c>: Send + Future<Output = Result<LookupReply>>
     where
         Self: 'c;
@@ -22,14 +25,24 @@ pub trait FsProvider {
         Self: 'c;
     fn open<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Open) -> Self::OpenFut<'c>;
 
-    fn read<'c, R, F>(&'c self, from: &'c Arc<BlockPath>, msg: Read, callback: F) -> Result<R>
+    fn read<'c, R, F>(&'c self, from: &'c Arc<BlockPath>, msg: ReadMsg, callback: F) -> Result<R>
     where
-        F: 'c + Send + FnOnce(&[u8]) -> R;
+        F: 'c + FnOnce(&[u8]) -> R;
 
     type WriteFut<'c>: Send + Future<Output = Result<WriteReply>>
     where
         Self: 'c;
-    fn write<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Write) -> Self::WriteFut<'c>;
+    fn write<'c, R>(
+        &'c self,
+        from: &'c Arc<BlockPath>,
+        inode: Inode,
+        handle: Handle,
+        offset: u64,
+        size: u64,
+        reader: R,
+    ) -> Self::WriteFut<'c>
+    where
+        R: 'c + Read;
 
     type FlushFut<'c>: Send + Future<Output = Result<()>>
     where
@@ -41,7 +54,7 @@ pub trait FsProvider {
         Self: 'c;
     fn read_dir<'c>(&'c self, from: &'c Arc<BlockPath>, msg: ReadDir) -> Self::ReadDirFut<'c>;
 
-    type LinkFut<'c>: Send + Future<Output = Result<()>>
+    type LinkFut<'c>: Send + Future<Output = Result<LinkReply>>
     where
         Self: 'c;
     fn link<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Link) -> Self::LinkFut<'c>;
@@ -51,17 +64,22 @@ pub trait FsProvider {
         Self: 'c;
     fn unlink<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Unlink) -> Self::UnlinkFut<'c>;
 
-    type ReadMetaFut<'c>: Send + Future<Output = Result<BlockMeta>>
+    type ReadMetaFut<'c>: Send + Future<Output = Result<ReadMetaReply>>
     where
         Self: 'c;
     fn read_meta<'c>(&'c self, from: &'c Arc<BlockPath>, msg: ReadMeta) -> Self::ReadMetaFut<'c>;
 
-    type WriteMetaFut<'c>: Send + Future<Output = Result<()>>
+    type WriteMetaFut<'c>: Send + Future<Output = Result<WriteMetaReply>>
     where
         Self: 'c;
     fn write_meta<'c>(&'c self, from: &'c Arc<BlockPath>, msg: WriteMeta)
         -> Self::WriteMetaFut<'c>;
 
+    type AllocateFut<'c>: Send + Future<Output = Result<()>>
+    where
+        Self: 'c;
+    fn allocate<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Allocate) -> Self::AllocateFut<'c>;
+
     type CloseFut<'c>: Send + Future<Output = Result<()>>
     where
         Self: 'c;
@@ -83,6 +101,105 @@ pub trait FsProvider {
     fn unlock<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Unlock) -> Self::UnlockFut<'c>;
 }
 
+impl<P: FsProvider> FsProvider for &P {
+    type LookupFut<'c> = P::LookupFut<'c> where Self: 'c;
+    fn lookup<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Lookup<'c>) -> Self::LookupFut<'c> {
+        (*self).lookup(from, msg)
+    }
+
+    type CreateFut<'c> = P::CreateFut<'c> where Self: 'c;
+    fn create<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Create<'c>) -> Self::CreateFut<'c> {
+        (*self).create(from, msg)
+    }
+
+    type OpenFut<'c> = P::OpenFut<'c> where Self: 'c;
+    fn open<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Open) -> Self::OpenFut<'c> {
+        (*self).open(from, msg)
+    }
+
+    fn read<'c, R, F>(&'c self, from: &'c Arc<BlockPath>, msg: ReadMsg, callback: F) -> Result<R>
+    where
+        F: 'c + FnOnce(&[u8]) -> R,
+    {
+        (*self).read(from, msg, callback)
+    }
+
+    type WriteFut<'c> = P::WriteFut<'c> where Self: 'c;
+    fn write<'c, R>(
+        &'c self,
+        from: &'c Arc<BlockPath>,
+        inode: Inode,
+        handle: Handle,
+        offset: u64,
+        size: u64,
+        reader: R,
+    ) -> Self::WriteFut<'c>
+    where
+        R: 'c + Read,
+    {
+        (*self).write(from, inode, handle, offset, size, reader)
+    }
+
+    type FlushFut<'c> = P::FlushFut<'c> where Self: 'c;
+    fn flush<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Flush) -> Self::FlushFut<'c> {
+        (*self).flush(from, msg)
+    }
+
+    type ReadDirFut<'c> = P::ReadDirFut<'c> where Self: 'c;
+    fn read_dir<'c>(&'c self, from: &'c Arc<BlockPath>, msg: ReadDir) -> Self::ReadDirFut<'c> {
+        (*self).read_dir(from, msg)
+    }
+
+    type LinkFut<'c> = P::LinkFut<'c> where Self: 'c;
+    fn link<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Link) -> Self::LinkFut<'c> {
+        (*self).link(from, msg)
+    }
+
+    type UnlinkFut<'c> = P::UnlinkFut<'c> where Self: 'c;
+    fn unlink<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Unlink) -> Self::UnlinkFut<'c> {
+        (*self).unlink(from, msg)
+    }
+
+    type ReadMetaFut<'c> = P::ReadMetaFut<'c> where Self: 'c;
+    fn read_meta<'c>(&'c self, from: &'c Arc<BlockPath>, msg: ReadMeta) -> Self::ReadMetaFut<'c> {
+        (*self).read_meta(from, msg)
+    }
+
+    type WriteMetaFut<'c> = P::WriteMetaFut<'c> where Self: 'c;
+    fn write_meta<'c>(
+        &'c self,
+        from: &'c Arc<BlockPath>,
+        msg: WriteMeta,
+    ) -> Self::WriteMetaFut<'c> {
+        (*self).write_meta(from, msg)
+    }
+
+    type AllocateFut<'c> = P::AllocateFut<'c> where Self: 'c;
+    fn allocate<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Allocate) -> Self::AllocateFut<'c> {
+        (*self).allocate(from, msg)
+    }
+
+    type CloseFut<'c> = P::CloseFut<'c> where Self: 'c;
+    fn close<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Close) -> Self::CloseFut<'c> {
+        (*self).close(from, msg)
+    }
+
+    type ForgetFut<'c> = P::ForgetFut<'c> where Self: 'c;
+    fn forget<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Forget) -> Self::ForgetFut<'c> {
+        (*self).forget(from, msg)
+    }
+
+    type LockFut<'c> = P::LockFut<'c> where Self: 'c;
+    fn lock<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Lock) -> Self::LockFut<'c> {
+        (*self).lock(from, msg)
+    }
+
+    type UnlockFut<'c> = P::UnlockFut<'c> where Self: 'c;
+    fn unlock<'c>(&'c self, from: &'c Arc<BlockPath>, msg: Unlock) -> Self::UnlockFut<'c> {
+        (*self).unlock(from, msg)
+    }
+}
+
 struct ServerCallback<P> {
     provider: Arc<P>,
 }
@@ -119,18 +236,30 @@ impl<P: 'static + Send + Sync + FsProvider> MsgCallback for ServerCallback<P> {
                             .block_on(replier.reply(FsReply::Read(ReadReply { data })))
                     })?;
                 }
-                FsMsg::Write(write) => FsReply::Write(provider.write(&from, write).await?),
+                FsMsg::Write(Write {
+                    inode,
+                    handle,
+                    offset,
+                    data,
+                }) => FsReply::Write(
+                    provider
+                        .write(&from, inode, handle, offset, data.len() as u64, data)
+                        .await?,
+                ),
                 FsMsg::Flush(flush) => FsReply::Ack(provider.flush(&from, flush).await?),
                 FsMsg::ReadDir(read_dir) => {
                     FsReply::ReadDir(provider.read_dir(&from, read_dir).await?)
                 }
-                FsMsg::Link(link) => FsReply::Ack(provider.link(&from, link).await?),
+                FsMsg::Link(link) => FsReply::Link(provider.link(&from, link).await?),
                 FsMsg::Unlink(unlink) => FsReply::Ack(provider.unlink(&from, unlink).await?),
                 FsMsg::ReadMeta(read_meta) => {
                     FsReply::ReadMeta(provider.read_meta(&from, read_meta).await?)
                 }
                 FsMsg::WriteMeta(write_meta) => {
-                    FsReply::Ack(provider.write_meta(&from, write_meta).await?)
+                    FsReply::WriteMeta(provider.write_meta(&from, write_meta).await?)
+                }
+                FsMsg::Allocate(allocate) => {
+                    FsReply::Ack(provider.allocate(&from, allocate).await?)
                 }
                 FsMsg::Close(close) => FsReply::Ack(provider.close(&from, close).await?),
                 FsMsg::Forget(forget) => FsReply::Ack(provider.forget(&from, forget).await?),

+ 10 - 1
crates/btfuse/Cargo.toml

@@ -9,10 +9,19 @@ edition = "2021"
 btlib = { path = "../btlib" }
 swtpm-harness = { path = "../swtpm-harness" }
 tss-esapi = { version = "7.1.0", features = ["generate-bindings"] }
-fuse-backend-rs = "0.9.6"
+btfproto = { path = "../btfproto" }
+tokio = { version = "1.24.2", features = ["rt", "rt-multi-thread"] }
+fuse-backend-rs = { version = "0.9.6", features = ["async-io"] }
 log = "0.4.17"
 env_logger = "0.9.0"
+anyhow = { version = "1.0.66", features = ["std", "backtrace"] }
+libc = { version = "0.2.137" }
+bytes = { version = "1.3.0" }
+serde_json = "1.0.92"
+futures = "0.3.25"
 
 [dev-dependencies]
+btfproto-tests = { path = "../btfproto-tests" }
 tempdir = "0.3.7"
 libc = "0.2.137"
+ctor = { version = "0.1.22" }

+ 126 - 0
crates/btfuse/src/fuse_daemon.rs

@@ -0,0 +1,126 @@
+use crate::{fuse_fs::FuseFs, PathExt, MOUNT_OPTIONS};
+
+use btfproto::server::FsProvider;
+use btlib::{
+    BlockPath, Result,
+};
+use fuse_backend_rs::{
+    api::server::Server,
+    transport::{self, FuseSession, FuseChannel},
+};
+use log::error;
+use std::path::PathBuf;
+use std::{
+    collections::HashMap,
+    ffi::{c_char, c_int, CString},
+    fs::File,
+    os::{fd::FromRawFd, unix::ffi::OsStrExt},
+    path::Path,
+    sync::Arc,
+};
+use tokio::{
+    task::JoinSet,
+};
+use futures::future::FutureExt;
+
+pub use private::FuseDaemon;
+
+mod private {
+    use super::*;
+
+    #[link(name = "fuse3")]
+    extern "C" {
+        /// Opens a channel to the kernel.
+        fn fuse_open_channel(mountpoint: *const c_char, options: *const c_char) -> c_int;
+    }
+
+    /// Calls into libfuse3 to mount this file system at the given path. The file descriptor to use
+    /// to communicate with the kernel is returned.
+    fn mount_at<P: AsRef<Path>>(mnt_point: P) -> File {
+        let mountpoint = CString::new(mnt_point.as_ref().as_os_str().as_bytes()).unwrap();
+        let options = CString::new(MOUNT_OPTIONS).unwrap();
+        let raw_fd = unsafe { fuse_open_channel(mountpoint.as_ptr(), options.as_ptr()) };
+        unsafe { File::from_raw_fd(raw_fd) }
+    }
+
+    pub struct FuseDaemon {
+        set: JoinSet<()>,
+    }
+
+    impl FuseDaemon {
+        const FSNAME: &str = "btfuse";
+        const FSTYPE: &str = "bt";
+        const NUM_THREADS: usize = 8;
+
+        pub fn new<T: AsRef<Path>, P: 'static + FsProvider>(
+            path: T,
+            uid_map: HashMap<u32, Arc<BlockPath>>,
+            fallback_path: Arc<BlockPath>,
+            provider: P,
+        ) -> Result<Self> {
+            path.try_create_dir()?;
+            let server = Arc::new(Server::new(FuseFs::new(provider, uid_map, fallback_path)));
+            let session = Self::session(path)?;
+            let mut set = JoinSet::new();
+            for _ in 0..Self::NUM_THREADS {
+                let server = server.clone();
+                let channel = session.new_channel()?;
+                let future = tokio::task::spawn_blocking(move || Self::server_loop(server, channel));
+                let future = future.map(|result| {
+                    if let Err(err) = result {
+                        error!("server_loop produced an error: {err}");
+                    }
+                });
+                set.spawn(future);
+            }
+            Ok(Self { set })
+        }
+
+        pub fn mnt_path<T: AsRef<Path>>(path: T) -> PathBuf {
+            path.as_ref().join("mnt")
+        }
+
+        fn session<T: AsRef<Path>>(path: T) -> Result<FuseSession> {
+            let mnt_path = Self::mnt_path(path);
+            mnt_path.try_create_dir()?;
+            let mut session =
+                FuseSession::new(mnt_path.as_ref(), Self::FSNAME, Self::FSTYPE, false)?;
+            session.set_fuse_file(mount_at(mnt_path));
+            Ok(session)
+        }
+
+        /// Opens a channel to the kernel and processes messages received in an infinite loop.
+        ///
+        /// # Warning
+        /// Because the future returned by this method is passed to `unsafe_cast`, you must not
+        /// hold any type which is not `Send` and `Sync` across an `await` point.
+        fn server_loop<P: 'static + FsProvider>(
+            server: Arc<Server<FuseFs<P>>>,
+            mut channel: FuseChannel,
+        ) {
+            loop {
+                match channel.get_request() {
+                    Ok(Some((reader, writer))) => {
+                        // Safety: reader and writer are not mutated while the future returned from
+                        // async_handle_message is alive.
+                        if let Err(err) = server.handle_message(reader, writer.into(), None, None) {
+                            error!("error while handling FUSE message: {err}");
+                        }
+                    }
+                    Ok(None) => break,
+                    Err(err) => {
+                        match err {
+                            // Occurs when the file system is unmounted.
+                            transport::Error::SessionFailure(_) => break,
+                            _ => error!("unexpected  error from Channel::get_request: {err}"),
+                        }
+                    }
+                }
+            }
+        }
+
+        pub async fn finished(&mut self) {
+            while self.set.join_next().await.is_some() {}
+        }
+    }
+}

+ 590 - 0
crates/btfuse/src/fuse_fs.rs

@@ -0,0 +1,590 @@
+use btfproto::{msg::*, server::FsProvider};
+use btlib::{
+    error::DisplayErr,
+    bterr, BlockId, Result, BlockMetaSecrets, BlockPath, Epoch,
+};
+use core::{ffi::CStr, time::Duration};
+use fuse_backend_rs::{
+    abi::fuse_abi::{stat64, Attr, CreateIn},
+    api::filesystem::{
+        Context, Entry, FileSystem,
+        OpenOptions, SetattrValid, FsOptions,
+    },
+};
+use log::{debug, error};
+use core::future::Future;
+use std::{
+    io::{self, Result as IoResult},
+    collections::HashMap,
+    sync::Arc,
+};
+
+pub use private::FuseFs;
+
+mod private {
+    use fuse_backend_rs::api::filesystem::{ZeroCopyWriter, ZeroCopyReader, DirEntry};
+    use tokio::runtime::Handle;
+
+    use super::*;
+
+    trait BlockMetaSecretsExt {
+        fn attr(&self) -> Result<Attr>;
+
+        fn stat(&self) -> Result<stat64> {
+            self.attr().map(|e| e.into())
+        }
+    }
+
+    impl BlockMetaSecretsExt for BlockMetaSecrets {
+        fn attr(&self) -> Result<Attr> {
+            Ok(Attr {
+                ino: self.block_id.inode,
+                size: self.size,
+                atime: self.atime.value(),
+                mtime: self.mtime.value(),
+                ctime: self.ctime.value(),
+                atimensec: 0,
+                mtimensec: 0,
+                ctimensec: 0,
+                mode: self.mode,
+                nlink: self.nlink,
+                uid: self.uid,
+                gid: self.gid,
+                rdev: 0,
+                blksize: self.sect_sz.try_into().map_err(|_| {
+                    bterr!("BlockMetaSecrets::sect_sz could not be converted to a u32")
+                })?,
+                blocks: self.sectors(),
+                flags: 0,
+            })
+        }
+    }
+    trait EntryExt {
+        fn fuse_entry(&self) -> Result<Entry>;
+    }
+
+    impl EntryExt for btfproto::msg::Entry {
+        fn fuse_entry(&self) -> Result<Entry> {
+            let btfproto::msg::Entry {
+                attr,
+                attr_timeout,
+                entry_timeout,
+            } = self;
+            let BlockId { inode, generation } = attr.block_id;
+            let attr = attr.stat()?;
+            Ok(Entry {
+                inode,
+                generation,
+                attr,
+                attr_flags: 0,
+                attr_timeout: *attr_timeout,
+                entry_timeout: *entry_timeout,
+            })
+        }
+    }
+
+    fn block_on<F: Future>(future: F) -> F::Output
+    {
+        Handle::current().block_on(future)
+    }
+
+    pub struct FuseFs<P> {
+        provider: P,
+        uid_map: HashMap<u32, Arc<BlockPath>>,
+        fallback_path: Arc<BlockPath>,
+    }
+
+    impl<P> FuseFs<P> {
+        fn path(&self, uid: u32) -> &Arc<BlockPath> {
+            self.uid_map.get(&uid).unwrap_or(&self.fallback_path)
+        }
+    }
+
+    impl<P: 'static + FsProvider> FuseFs<P> {
+        pub fn new(
+            provider: P,
+            uid_map: HashMap<u32, Arc<BlockPath>>,
+            fallback_path: Arc<BlockPath>,
+        ) -> Self {
+            Self {
+                provider,
+                uid_map,
+                fallback_path,
+            }
+        }
+    }
+
+    unsafe impl<P: Sync> Sync for FuseFs<P> {}
+
+    impl<P: 'static + FsProvider> FileSystem for FuseFs<P> {
+        type Inode = btfproto::Inode;
+        type Handle = btfproto::Handle;
+
+        fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
+            Ok(FsOptions::empty())
+        }
+
+        fn lookup(
+            &self,
+            ctx: &Context,
+            parent: Self::Inode,
+            name: &CStr,
+        ) -> IoResult<Entry> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let name = name.to_str().display_err()?;
+                let msg = Lookup { parent, name };
+                let entry = match self.provider.lookup(path, msg).await {
+                    Ok(LookupReply { entry, .. }) => entry,
+                    Err(err) => return match err.downcast::<io::Error>() {
+                        Ok(err) => {
+                            debug!("lookup returned io::Error: {err}");
+                            Err(err)
+                        },
+                        Err(err) => Err(io::Error::new(io::ErrorKind::Other, err.to_string())),
+                    }
+                };
+                let entry = entry.fuse_entry()?;
+                Ok(entry)
+            })
+        }
+
+        fn create(
+            &self,
+            ctx: &Context,
+            parent: Self::Inode,
+            name: &CStr,
+            args: CreateIn,
+        ) -> IoResult<(Entry, Option<Self::Handle>, OpenOptions)> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let name = name.to_str().display_err()?;
+                let msg = Create {
+                    parent,
+                    name,
+                    flags: Flags::new(args.flags as i32),
+                    mode: args.mode,
+                    umask: args.umask,
+                };
+                let CreateReply { entry, handle, .. } = self.provider.create(path, msg).await?;
+                let entry = entry.fuse_entry()?;
+                Ok((entry, Some(handle), OpenOptions::empty()))
+            })
+        }
+
+        fn open(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            flags: u32,
+            _fuse_flags: u32,
+        ) -> IoResult<(Option<Self::Handle>, OpenOptions)> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let msg = Open {
+                    inode,
+                    flags: Flags::new(flags as i32),
+                };
+                let handle = match self.provider.open(path, msg).await {
+                    Ok(OpenReply { handle, .. }) => handle,
+                    Err(err) => {
+                        error!("FsProvider::open returned an error: {err}");
+                        return Err(err.into());
+                    }
+                };
+                Ok((Some(handle), OpenOptions::empty()))
+            })
+        }
+
+        fn opendir(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            flags: u32,
+        ) -> IoResult<(Option<Self::Handle>, OpenOptions)> {
+            let flags = flags | libc::O_DIRECTORY as u32;
+            self.open(ctx, inode, flags, 0)
+        }
+
+        fn read(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            handle: Self::Handle,
+            w: &mut dyn ZeroCopyWriter,
+            size: u32,
+            offset: u64,
+            _lock_owner: Option<u64>,
+            _flags: u32,
+        ) -> IoResult<usize> {
+            let path = self.path(ctx.uid);
+            let msg = Read {
+                inode,
+                handle,
+                offset,
+                size: size as u64,
+            };
+            self.provider.read(path, msg, |data| w.write(data))?
+        }
+
+        fn write(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            handle: Self::Handle,
+            r: &mut dyn ZeroCopyReader,
+            size: u32,
+            offset: u64,
+            _lock_owner: Option<u64>,
+            _delayed_write: bool,
+            _flags: u32,
+            _fuse_flags: u32,
+        ) -> IoResult<usize> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let size: usize = size.try_into().display_err()?;
+                let WriteReply { written, .. } = self
+                    .provider
+                    .write(path, inode, handle, offset, size as u64, r)
+                    .await?;
+                Ok(written.try_into().display_err()?)
+            })
+        }
+
+        fn flush(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            handle: Self::Handle,
+            _lock_owner: u64,
+        ) -> io::Result<()> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let msg = Flush { inode, handle };
+                self.provider.flush(path, msg).await?;
+                Ok(())
+            })
+        }
+
+        fn readdir(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            handle: Self::Handle,
+            size: u32,
+            offset: u64,
+            add_entry: &mut dyn FnMut(DirEntry) -> io::Result<usize>,
+        ) -> io::Result<()> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let msg = ReadDir {
+                    inode,
+                    handle,
+                    limit: 0,
+                    state: offset,
+                };
+                let ReadDirReply { entries, .. } = self.provider.read_dir(path, msg).await?;
+                let mut size: usize = size.try_into().display_err()?;
+                for (index, (name, entry)) in entries.into_iter().enumerate() {
+                    let inode = match entry.inode() {
+                        Some(inode) => inode,
+                        None => continue,
+                    };
+                    let offset = (index as u64) + 1;
+                    let dir_entry = DirEntry {
+                        ino: inode,
+                        offset,
+                        type_: entry.kind() as u32,
+                        name: name.as_bytes()
+                    };
+                    size = size.saturating_sub(add_entry(dir_entry)?);
+                    if size == 0 {
+                        break;
+                    }
+                }
+                Ok(())
+            })
+        }
+
+        fn link(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            new_parent: Self::Inode,
+            new_name: &CStr,
+        ) -> io::Result<Entry> {
+            debug!("link called");
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let name = new_name.to_str().display_err()?;
+                let msg = Link {
+                    inode,
+                    new_parent,
+                    name,
+                };
+                let LinkReply { entry, .. } = self.provider.link(path, msg).await?;
+                let entry = entry.fuse_entry()?;
+                Ok(entry)
+            })
+        }
+
+        fn unlink(&self, ctx: &Context, parent: Self::Inode, name: &CStr) -> io::Result<()> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let name = name.to_str().display_err()?;
+                let msg = Unlink {
+                    parent,
+                    name,
+                };
+                self.provider.unlink(path, msg).await?;
+                Ok(())
+            })
+        }
+
+        fn getattr(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            handle: Option<Self::Handle>,
+        ) -> IoResult<(stat64, Duration)> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let msg = ReadMeta { inode, handle };
+                let ReadMetaReply {
+                    meta, valid_for, ..
+                } = self.provider.read_meta(path, msg).await?;
+                let stat = meta.body().secrets()?.stat()?;
+                Ok((stat, valid_for))
+            })
+        }
+
+        fn setattr(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            attr: stat64,
+            handle: Option<Self::Handle>,
+            valid: SetattrValid,
+        ) -> IoResult<(stat64, Duration)> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let mut msg_attrs = Attrs::default();
+                let mut attrs_set = AttrsSet::none();
+                if valid.intersects(SetattrValid::MODE) {
+                    msg_attrs.mode = attr.st_mode;
+                    attrs_set |= AttrsSet::MODE;
+                }
+                if valid.intersects(SetattrValid::UID) {
+                    msg_attrs.uid = attr.st_uid;
+                    attrs_set |= AttrsSet::UID;
+                }
+                if valid.intersects(SetattrValid::GID) {
+                    msg_attrs.gid = attr.st_gid;
+                    attrs_set |= AttrsSet::GID;
+                }
+                if valid.intersects(SetattrValid::ATIME) {
+                    let atime: u64 = attr.st_atime.try_into().display_err()?;
+                    msg_attrs.atime = Epoch::from(atime);
+                    attrs_set |= AttrsSet::ATIME;
+                }
+                if valid.intersects(SetattrValid::MTIME) {
+                    let mtime: u64 = attr.st_mtime.try_into().display_err()?;
+                    msg_attrs.mtime = Epoch::from(mtime);
+                    attrs_set |= AttrsSet::MTIME;
+                }
+                if valid.intersects(SetattrValid::CTIME) {
+                    let ctime: u64 = attr.st_ctime.try_into().display_err()?;
+                    msg_attrs.ctime = Epoch::from(ctime);
+                    attrs_set |= AttrsSet::CTIME;
+                }
+                let msg = WriteMeta {
+                    inode,
+                    handle,
+                    attrs: msg_attrs,
+                    attrs_set,
+                };
+                let WriteMetaReply {
+                    attrs, valid_for, ..
+                } = self.provider.write_meta(path, msg).await?;
+                Ok((attrs.stat()?, valid_for))
+            })
+        }
+
+        fn fsync(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            _datasync: bool,
+            handle: Self::Handle,
+        ) -> IoResult<()> {
+            block_on(async move {
+                let path = self.path(ctx.uid);
+                let msg = Flush { inode, handle };
+                self.provider.flush(path, msg).await?;
+                Ok(())
+            })
+        }
+
+        fn fsyncdir(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            datasync: bool,
+            handle: Self::Handle,
+        ) -> IoResult<()> {
+            self.fsync(ctx, inode, datasync, handle)
+        }
+
+        fn fallocate(
+            &self,
+            ctx: &Context,
+            inode: Self::Inode,
+            handle: Self::Handle,
+            mode: u32,
+            offset: u64,
+            length: u64,
+        ) -> IoResult<()> {
+            block_on(async move {
+                if mode != 0 {
+                    error!("a non-zero mode argument was given to async_fallocate: {mode}");
+                    return Err(io::Error::from_raw_os_error(libc::ENOTSUP));
+                }
+                let path = self.path(ctx.uid);
+                let msg = Allocate {
+                    inode,
+                    handle,
+                    offset,
+                    size: length,
+                };
+                self.provider.allocate(path, msg).await?;
+                Ok(())
+            })
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use btfproto::Inode;
+    use btfproto_tests::local_fs::{ConcreteFs, LocalFsTest};
+    use fuse_backend_rs::api::filesystem::Context;
+    use std::{collections::HashMap, ffi::CString};
+    use tempdir::TempDir;
+
+    struct FuseFsTest {
+        _dir: TempDir,
+        fs: FuseFs<ConcreteFs>,
+    }
+
+    impl FuseFsTest {
+        fn new_empty() -> Self {
+            let case = LocalFsTest::new_empty();
+            let from = case.from().to_owned();
+            let (dir, fs, ..) = case.into_parts();
+            let fs = FuseFs::new(fs, HashMap::new(), from);
+            Self { _dir: dir, fs }
+        }
+
+        fn fs(&self) -> &FuseFs<ConcreteFs> {
+            &self.fs
+        }
+
+        fn ctx(&self) -> Context {
+            Context {
+                uid: 0,
+                gid: 0,
+                pid: 0,
+            }
+        }
+    }
+
+    #[tokio::test]
+    async fn lookup_file_exists() {
+        tokio::task::spawn_blocking(|| {
+            let case = FuseFsTest::new_empty();
+            let fuse_fs = case.fs();
+            let root: Inode = SpecInodes::RootDir.into();
+            let ctx = case.ctx();
+            let name = CString::new("file.txt").unwrap();
+
+            let args = CreateIn {
+                flags: libc::O_RDWR as u32,
+                mode: 0o644,
+                umask: 0,
+                fuse_flags: 0,
+            };
+            let (expected, ..) = fuse_fs.create(&ctx, root, &name, args).unwrap();
+            let actual = fuse_fs.lookup(&ctx, root, &name).unwrap();
+
+            assert_eq!(expected.inode, actual.inode);
+            assert_eq!(expected.generation, actual.generation);
+            assert_eq!(expected.attr, actual.attr);
+            assert_eq!(expected.attr_flags, actual.attr_flags);
+            assert_eq!(expected.attr_timeout, actual.attr_timeout);
+            assert_eq!(expected.entry_timeout, actual.entry_timeout);
+        }).await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn setattr() {
+        macro_rules! check {
+            (
+                $actual:ident,
+                $entry:ident,
+                $getattr_return:ident,
+                $setattr_return:ident,
+                $field:ident
+            ) => {
+                assert_ne!($actual.$field, $entry.attr.$field);
+                assert_eq!($actual.$field, $setattr_return.$field);
+                assert_eq!($actual.$field, $getattr_return.$field);
+            };
+        }
+
+        tokio::task::spawn_blocking(move || {
+            let case = FuseFsTest::new_empty();
+            let fuse_fs = case.fs();
+            let root: Inode = SpecInodes::RootDir.into();
+            let ctx = case.ctx();
+            let name = CString::new("file.txt").unwrap();
+
+            let args = CreateIn {
+                flags: libc::O_RDWR as u32,
+                mode: 0o644,
+                umask: 0,
+                fuse_flags: 0,
+            };
+            let (entry, handle, ..) = fuse_fs.create(&ctx, root, &name, args).unwrap();
+            let inode = entry.inode;
+            let actual = {
+                let mut actual = stat64::from(Attr::default());
+                actual.st_mode = 0o777;
+                actual.st_uid = 2372;
+                actual.st_gid = 2312;
+                actual.st_atime = 21323;
+                actual.st_mtime = 21290;
+                actual.st_ctime = 119200;
+                actual
+            };
+            let valid = SetattrValid::MODE
+                | SetattrValid::UID
+                | SetattrValid::GID
+                | SetattrValid::ATIME
+                | SetattrValid::MTIME
+                | SetattrValid::CTIME;
+            let (setattr_return, ..) = fuse_fs
+                .setattr(&ctx, inode, actual, handle, valid)
+                .unwrap();
+            let (getattr_return, ..) = fuse_fs.getattr(&ctx, inode, handle).unwrap();
+
+            check!(actual, entry, getattr_return, setattr_return, st_mode);
+            check!(actual, entry, getattr_return, setattr_return, st_uid);
+            check!(actual, entry, getattr_return, setattr_return, st_gid);
+            check!(actual, entry, getattr_return, setattr_return, st_atime);
+            check!(actual, entry, getattr_return, setattr_return, st_mtime);
+            check!(actual, entry, getattr_return, setattr_return, st_ctime);
+        }).await.unwrap();
+    }
+}

+ 310 - 324
crates/btfuse/src/main.rs

@@ -1,24 +1,27 @@
+mod fuse_daemon;
+use fuse_daemon::FuseDaemon;
+mod fuse_fs;
+
+use btfproto::{local_fs::LocalFs, server::FsProvider};
 use btlib::{
-    blocktree::{Blocktree, ModeAuthorizer},
     crypto::{
         tpm::{TpmCredStore, TpmCreds},
-        CredStore,
+        CredStore, Creds, CredsPriv,
     },
+    BlockPath, Result,
 };
-use fuse_backend_rs::{
-    api::server::Server,
-    transport::{Error, FuseSession},
-};
-use log::error;
+use core::future::Future;
+use log::{error};
+use serde_json::from_str;
 use std::{
-    ffi::{c_char, CString},
-    fs::{self, File},
+    collections::HashMap,
+    env::VarError,
+    ffi::OsString,
+    fs::{self},
     io,
-    os::fd::FromRawFd,
-    os::{raw::c_int, unix::ffi::OsStrExt},
     path::{Path, PathBuf},
     str::FromStr,
-    sync::mpsc::Sender,
+    sync::Arc,
 };
 use tss_esapi::{
     tcti_ldr::{TabrmdConfig, TctiNameConf},
@@ -27,8 +30,8 @@ use tss_esapi::{
 
 const DEFAULT_TABRMD: &str = "bus_type=session";
 const MOUNT_OPTIONS: &str = "default_permissions";
-const FSNAME: &str = "btfuse";
-const FSTYPE: &str = "bt";
+const TABRMD_ENVVAR: &str = "BT_TABRMD";
+const UIDMAP_ENVVAR: &str = "BT_UIDMAP";
 
 trait PathExt {
     fn try_create_dir(&self) -> io::Result<()>;
@@ -46,137 +49,99 @@ impl<T: AsRef<Path>> PathExt for T {
     }
 }
 
-#[link(name = "fuse3")]
-extern "C" {
-    /// Opens a channel to the kernel.
-    fn fuse_open_channel(mountpoint: *const c_char, options: *const c_char) -> c_int;
-}
-
-/// Calls into libfuse3 to mount this file system at the given path. The file descriptor to use
-/// to communicate with the kernel is returned.
-fn mount_at<P: AsRef<Path>>(mnt_point: P) -> File {
-    let mountpoint = CString::new(mnt_point.as_ref().as_os_str().as_bytes()).unwrap();
-    let options = CString::new(MOUNT_OPTIONS).unwrap();
-    let raw_fd = unsafe { fuse_open_channel(mountpoint.as_ptr(), options.as_ptr()) };
-    unsafe { File::from_raw_fd(raw_fd) }
+fn tpm_state_path<T: AsRef<Path>>(main_dir: T) -> PathBuf {
+    main_dir.as_ref().join("tpm_state")
 }
 
-/// A fuse daemon process.
-struct FuseDaemon<'a> {
-    /// The path of the directory to store all file system information in.
-    path: &'a Path,
-    /// The configuration string to use to connect to a Tabrmd instance.
-    tabrmd_config: &'a str,
-    /// An optional [Sender] which is called when this daemon has finished starting up.
-    started_signal: Option<Sender<()>>,
+fn block_dir<T: AsRef<Path>>(main_dir: T) -> PathBuf {
+    main_dir.as_ref().join("bt")
 }
 
-impl<'a> FuseDaemon<'a> {
-    fn new(path: &'a Path, tabrmd_config: &'a str) -> FuseDaemon<'a> {
-        FuseDaemon {
-            path,
-            tabrmd_config,
-            started_signal: None,
-        }
-    }
-
-    fn tpm_state_path<P: AsRef<Path>>(path: P) -> PathBuf {
-        path.as_ref().join("tpm_state")
-    }
-
-    fn mnt_path<P: AsRef<Path>>(path: P) -> PathBuf {
-        path.as_ref().join("mnt")
-    }
-
-    fn server<P: AsRef<Path>>(&self, mnt_path: P) -> Server<Blocktree<TpmCreds, ModeAuthorizer>> {
-        let empty = fs::read_dir(&mnt_path)
-            .expect("failed to read mountdir")
-            .next()
-            .is_none();
-        let context = Context::new(TctiNameConf::Tabrmd(
-            TabrmdConfig::from_str(self.tabrmd_config).expect("failed to parse Tabrmd config"),
-        ))
-        .expect("failed to connect to Tabrmd");
-        let cred_store = TpmCredStore::new(context, Self::tpm_state_path(self.path))
-            .expect("failed to create TpmCredStore");
-        let node_creds = cred_store
-            .node_creds()
-            .expect("failed to retrieve node creds");
-        let bt_path = self.path.join("bt");
-        bt_path
-            .try_create_dir()
-            .expect("failed to create blocktree directory");
-        let fs = if empty {
-            Blocktree::new_empty(bt_path, 0, node_creds, ModeAuthorizer {})
-        } else {
-            Blocktree::new_existing(bt_path, node_creds, ModeAuthorizer {})
-        }
-        .expect("failed to create blocktree");
-
-        Server::new(fs)
+fn parse_uid_map(uid_map: &str) -> Result<HashMap<u32, Arc<BlockPath>>> {
+    let map: HashMap<u32, BlockPath> = from_str(uid_map)?;
+    let mut output = HashMap::with_capacity(map.len());
+    for (key, value) in map.into_iter() {
+        output.insert(key, Arc::new(value));
     }
+    Ok(output)
+}
 
-    fn fuse_session<P: AsRef<Path>>(&self, mnt_path: P) -> FuseSession {
-        self.path
-            .try_create_dir()
-            .expect("failed to create main directory");
-        let mut session = FuseSession::new(mnt_path.as_ref(), FSNAME, FSTYPE, false)
-            .expect("failed to create FUSE session");
-        session.set_fuse_file(mount_at(mnt_path));
+fn node_creds(state_file: PathBuf, tabrmd_cfg: &str) -> Result<TpmCreds> {
+    let context = Context::new(TctiNameConf::Tabrmd(TabrmdConfig::from_str(tabrmd_cfg)?))?;
+    let cred_store = TpmCredStore::new(context, state_file)?;
+    cred_store.node_creds()
+}
 
-        session
+fn provider<C: 'static + Creds + Send + Sync>(
+    btdir: PathBuf,
+    node_creds: C,
+) -> Result<impl FsProvider> {
+    btdir.try_create_dir()?;
+    let empty = fs::read_dir(&btdir)?.next().is_none();
+    if empty {
+        LocalFs::new_empty(btdir, 0, node_creds, btfproto::local_fs::ModeAuthorizer {})
+    } else {
+        LocalFs::new_existing(btdir, node_creds, btfproto::local_fs::ModeAuthorizer {})
     }
+}
 
-    fn start(&self) {
-        let mnt_path = Self::mnt_path(self.path);
-        mnt_path
-            .try_create_dir()
-            .expect("failed to create mount directory");
-        let server = self.server(&mnt_path);
-        let session = self.fuse_session(&mnt_path);
-        drop(mnt_path);
-        let mut channel = session
-            .new_channel()
-            .expect("failed to create FUSE channel");
-        if let Some(tx) = self.started_signal.as_ref() {
-            tx.send(()).expect("failed to send started signal");
-        }
+fn run_daemon(
+    main_dir: OsString,
+    tabrmd_string: Option<String>,
+    uid_map: Option<String>,
+    tpm_state_file: Option<PathBuf>,
+) -> impl Send + Sync + Future<Output = ()> {
+    let main_dir = PathBuf::from(main_dir);
+    let tabrmd_cfg = tabrmd_string
+        .as_ref()
+        .map_or(DEFAULT_TABRMD, |s| s.as_str());
+    let uid_map = {
+        let uid_map = uid_map.unwrap_or_else(|| "{}".to_owned());
+        parse_uid_map(&uid_map).unwrap()
+    };
+    let tpm_state_file = tpm_state_file.unwrap_or_else(|| tpm_state_path(&main_dir));
+    let node_creds = node_creds(tpm_state_file, tabrmd_cfg).expect("failed to get node creds");
+    let fallback_path = {
+        let writecap = node_creds
+            .writecap()
+            .ok_or(btlib::BlockError::MissingWritecap)
+            .unwrap();
+        Arc::new(writecap.bind_path())
+    };
+    let provider = {
+        let btdir = block_dir(&main_dir);
+        provider(btdir, node_creds).expect("failed to create FS provider")
+    };
 
-        loop {
-            match channel.get_request() {
-                Ok(Some((reader, writer))) => {
-                    if let Err(err) = server.handle_message(reader, writer.into(), None, None) {
-                        error!("error while handling FUSE message: {err}");
-                    }
-                }
-                Ok(None) => break,
-                Err(err) => {
-                    match err {
-                        // Occurs when the file system is unmounted.
-                        Error::SessionFailure(_) => break,
-                        _ => error!("{err}"),
-                    }
-                }
-            }
-        }
-    }
+    let mut daemon = FuseDaemon::new(main_dir, uid_map, fallback_path, provider)
+        .expect("failed to create FUSE daemon");
+    async move { daemon.finished().await }
 }
 
-fn main() {
+#[tokio::main]
+async fn main() {
     env_logger::init();
-    let main_dir = std::env::args().nth(1).expect("no mount point given");
-    let main_dir =
-        PathBuf::from_str(main_dir.as_str()).expect("failed to convert mount point to PathBuf");
-    let tabrmd_string = std::env::var("BT_TABRMD").ok();
-    let tabrmd_str = tabrmd_string
-        .as_ref()
-        .map_or(DEFAULT_TABRMD, |s| s.as_str());
-    let daemon = FuseDaemon::new(&main_dir, tabrmd_str);
-    daemon.start();
+    let main_dir = std::env::args_os().nth(1).expect("no mount point given");
+    let tabrmd_string = std::env::var(TABRMD_ENVVAR).ok();
+    let uid_map = match std::env::var(UIDMAP_ENVVAR) {
+        Ok(uid_map) => Some(uid_map),
+        Err(err) => match err {
+            VarError::NotPresent => None,
+            VarError::NotUnicode(_) => {
+                error!("environment variable {UIDMAP_ENVVAR} contained non-unicode characters");
+                return;
+            }
+        },
+    };
+    run_daemon(main_dir, tabrmd_string, uid_map, None).await;
 }
 
 #[cfg(test)]
 mod test {
+    use super::*;
+
+    use btlib::{crypto::Creds, log::BuilderExt, Epoch, Principaled};
+    use ctor::ctor;
     use std::{
         ffi::{OsStr, OsString},
         fs::{
@@ -184,16 +149,25 @@ mod test {
             set_permissions, write, Permissions, ReadDir,
         },
         os::unix::fs::PermissionsExt,
-        sync::mpsc::{channel, Receiver},
         thread::JoinHandle,
         time::Duration,
+        sync::mpsc,
     };
-
-    use btlib::{crypto::Creds, log::BuilderExt, Epoch, Principaled};
     use swtpm_harness::SwtpmHarness;
     use tempdir::TempDir;
 
-    use super::*;
+    /// An optional timeout to wait for the FUSE daemon to start in tests.
+    const TIMEOUT: Option<Duration> = Some(Duration::from_millis(1000));
+
+    /// The log level to use when running tests.
+    /// Note that the debug log level significantly reduces performance.
+    const LOG_LEVEL: &str = "warn";
+
+    #[ctor]
+    fn ctor() {
+        std::env::set_var("RUST_LOG", LOG_LEVEL);
+        env_logger::Builder::from_default_env().btformat().init();
+    }
 
     /// Unmounts the file system at the given path.
     fn unmount<P: AsRef<Path>>(mnt_path: P) {
@@ -218,61 +192,76 @@ mod test {
         read_dir.map(|entry| entry.unwrap().file_name())
     }
 
+    const ROOT_PASSWD: &str = "password";
+
     struct TestCase {
-        temp_path_rx: Receiver<PathBuf>,
-        mnt_path: Option<PathBuf>,
+        _temp_dir: TempDir,
+        mnt_path: PathBuf,
         handle: Option<JoinHandle<()>>,
     }
 
     impl TestCase {
-        const ROOT_PASSWD: &str = "Gnurlingwheel";
-
         fn new() -> TestCase {
-            let (tx, rx) = channel();
-            let (started_tx, started_rx) = channel();
-            let handle = std::thread::spawn(move || {
-                let dir = TempDir::new("btfuse").expect("failed to create TempDir");
-                tx.send(dir.path().to_owned()).expect("send failed");
-                let swtpm = SwtpmHarness::new().expect("failed to start swtpm");
-                {
-                    let context = swtpm.context().expect("failed to create TPM context");
-                    let cred_store =
-                        TpmCredStore::new(context, FuseDaemon::tpm_state_path(dir.path()))
-                            .expect("failed to create TpmCredStore");
-                    let root_creds = cred_store
-                        .gen_root_creds(Self::ROOT_PASSWD)
-                        .expect("failed to gen root creds");
-                    let mut node_creds = cred_store.node_creds().expect("failed to get node creds");
-                    let expires = Epoch::now() + Duration::from_secs(3600);
-                    let writecap = root_creds
-                        .issue_writecap(node_creds.principal(), vec![], expires)
-                        .expect("failed to issue writecap to node creds");
-                    cred_store
-                        .assign_node_writecap(&mut node_creds, writecap)
-                        .expect("failed to assign writecap");
-                }
-                let mut daemon = FuseDaemon::new(dir.path(), swtpm.tabrmd_config());
-                daemon.started_signal = Some(started_tx);
-                daemon.start()
-            });
-            started_rx
-                .recv_timeout(Duration::from_millis(1500))
-                .expect("failed to receive started signal from `FuseDaemon`");
-            TestCase {
-                temp_path_rx: rx,
-                mnt_path: None,
+            let tmp = TempDir::new("btfuse").unwrap();
+            let mnt_path = FuseDaemon::mnt_path(tmp.path());
+            let main_dir = OsString::from(tmp.path().to_owned());
+            let (mounted_tx, mounted_rx) = mpsc::channel();
+            let handle = std::thread::spawn(move || Self::run(main_dir, mounted_tx));
+            match TIMEOUT {
+                Some(duration) => mounted_rx.recv_timeout(duration).unwrap(),
+                None => mounted_rx.recv().unwrap(),
+            };
+            Self {
+                _temp_dir: tmp,
+                mnt_path,
                 handle: Some(handle),
             }
         }
 
-        fn mnt_path(&mut self) -> &PathBuf {
-            self.mnt_path.get_or_insert_with(|| {
-                let temp_path = self
-                    .temp_path_rx
-                    .recv_timeout(Duration::from_secs(1))
-                    .expect("receive failed");
-                FuseDaemon::mnt_path(temp_path)
-            })
+        fn run(main_dir: OsString, mounted_tx: mpsc::Sender<()>) {
+            let swtpm = Self::swtpm();
+            let state_path = swtpm.state_path().to_owned();
+            let tabrmd_string = Some(swtpm.tabrmd_config().to_owned());
+            let uid_map = None;
+            let runtime = tokio::runtime::Builder::new_current_thread()
+                .build()
+                .unwrap();
+            // run_daemon can only be called in the context of a runtime, hence the need to call
+            // spawn_blocking.
+            let started = runtime.spawn_blocking(|| run_daemon(
+                main_dir,
+                tabrmd_string,
+                uid_map,
+                Some(state_path),
+            ));
+            let future = runtime.block_on(started).unwrap();
+
+            // The file system is mounted before run_daemon returns.
+            mounted_tx.send(()).unwrap();
+            runtime.block_on(future);
+        }
+
+        fn swtpm() -> SwtpmHarness {
+            let swtpm = SwtpmHarness::new().unwrap();
+            let state_path: PathBuf = swtpm.state_path().to_owned();
+            let cred_store = {
+                let context = swtpm.context().unwrap();
+                TpmCredStore::new(context, state_path.clone()).unwrap()
+            };
+            let root_creds = cred_store.gen_root_creds(ROOT_PASSWD).unwrap();
+            let mut node_creds = cred_store.node_creds().unwrap();
+            let expires = Epoch::now() + Duration::from_secs(3600);
+            let writecap = root_creds
+                .issue_writecap(node_creds.principal(), vec![], expires)
+                .unwrap();
+            cred_store
+                .assign_node_writecap(&mut node_creds, writecap)
+                .unwrap();
+            swtpm
+        }
+
+        fn mnt_path(&self) -> &PathBuf {
+            &self.mnt_path
         }
 
         fn wait(&mut self) {
@@ -297,7 +286,7 @@ mod test {
 
     impl Drop for TestCase {
         fn drop(&mut self) {
-            self.unmount_and_wait();
+            self.unmount_and_wait()
         }
     }
 
@@ -306,12 +295,8 @@ mod test {
     //#[test]
     #[allow(dead_code)]
     fn manual_test() {
-        // The debug log level significantly reduces performance. You can speed things up by
-        // replacing "debug" with "warn".
-        std::env::set_var("RUST_LOG", "debug");
-        env_logger::Builder::from_default_env().btformat().init();
         let mut case = TestCase::new();
-        case.wait();
+        case.wait()
     }
 
     /// Tests if the file system can be mount then unmounted successfully.
@@ -321,16 +306,17 @@ mod test {
     }
 
     #[test]
-    fn write_read() {
+    fn write_read() -> Result<()> {
         const EXPECTED: &[u8] =
             b"The paths to failure are uncountable, yet to success there is but one.";
-        let mut case = TestCase::new();
+        let case = TestCase::new();
         let file_path = case.mnt_path().join("file");
 
-        write(&file_path, EXPECTED).expect("write failed");
+        write(&file_path, EXPECTED)?;
 
-        let actual = read(&file_path).expect("read failed");
+        let actual = read(&file_path)?;
         assert_eq!(EXPECTED, actual);
+        Ok(())
     }
 
     #[test]
@@ -338,7 +324,7 @@ mod test {
         const DATA: &[u8] = b"Au revoir Shoshanna!";
         let file_name = OsStr::new("landa_dialog.txt");
         let expected = [file_name];
-        let mut case = TestCase::new();
+        let case = TestCase::new();
         let mnt_path = case.mnt_path();
         let file_path = mnt_path.join(file_name);
 
@@ -354,7 +340,7 @@ mod test {
     fn create_then_delete_file() {
         const DATA: &[u8] = b"The universe is hostile, so impersonal. Devour to survive";
         let file_name = OsStr::new("tool_lyrics.txt");
-        let mut case = TestCase::new();
+        let case = TestCase::new();
         let mnt_path = case.mnt_path();
         let file_path = mnt_path.join(file_name);
         write(&file_path, DATA).expect("write failed");
@@ -365,155 +351,155 @@ mod test {
         assert!(file_names(read_dir(&mnt_path).expect("read_dir failed")).eq(expected))
     }
 
-    #[test]
-    fn hard_link_then_remove() {
-        const EXPECTED: &[u8] = b"And the lives we've reclaimed";
-        let name1 = OsStr::new("refugee_lyrics.txt");
-        let name2 = OsStr::new("rise_against_lyrics.txt");
-        let mut case = TestCase::new();
-        let mnt_path = case.mnt_path();
-        let path1 = mnt_path.join(name1);
-        let path2 = mnt_path.join(name2);
-        write(&path1, EXPECTED).expect("write failed");
-
-        hard_link(&path1, &path2).expect("hard_link failed");
-        remove_file(&path1).expect("remove_file failed");
-
-        let actual = read(&path2).expect("read failed");
-        assert_eq!(EXPECTED, actual);
-    }
-
-    #[test]
-    fn hard_link_then_remove_both() {
-        const EXPECTED: &[u8] = b"And the lives we've reclaimed";
-        let name1 = OsStr::new("refugee_lyrics.txt");
-        let name2 = OsStr::new("rise_against_lyrics.txt");
-        let mut case = TestCase::new();
-        let mnt_path = case.mnt_path();
-        let path1 = mnt_path.join(name1);
-        let path2 = mnt_path.join(name2);
-        write(&path1, EXPECTED).expect("write failed");
-
-        hard_link(&path1, &path2).expect("hard_link failed");
-        remove_file(&path1).expect("remove_file on path1 failed");
-        remove_file(&path2).expect("remove_file on path2 failed");
-
-        let expected: [&OsStr; 0] = [];
-        assert!(file_names(read_dir(&mnt_path).expect("read_dir failed")).eq(expected));
-    }
-
-    #[test]
-    fn set_mode_bits() {
-        const EXPECTED: u32 = libc::S_IFREG | 0o777;
-        let mut case = TestCase::new();
-        let file_path = case.mnt_path().join("bagobits");
-        write(&file_path, []).expect("write failed");
-        let original = metadata(&file_path)
-            .expect("metadata failed")
-            .permissions()
-            .mode();
-        assert_ne!(EXPECTED, original);
-
-        set_permissions(&file_path, Permissions::from_mode(EXPECTED))
-            .expect("set_permissions failed");
-
-        let actual = metadata(&file_path)
-            .expect("metadata failed")
-            .permissions()
-            .mode();
-        assert_eq!(EXPECTED, actual);
-    }
+    //#[test]
+    //fn hard_link_then_remove() {
+    //    const EXPECTED: &[u8] = b"And the lives we've reclaimed";
+    //    let name1 = OsStr::new("refugee_lyrics.txt");
+    //    let name2 = OsStr::new("rise_against_lyrics.txt");
+    //    let case = TestCase::new();
+    //    let mnt_path = case.mnt_path();
+    //    let path1 = mnt_path.join(name1);
+    //    let path2 = mnt_path.join(name2);
+    //    write(&path1, EXPECTED).expect("write failed");
+
+    //    hard_link(&path1, &path2).expect("hard_link failed");
+    //    remove_file(&path1).expect("remove_file failed");
+
+    //    let actual = read(&path2).expect("read failed");
+    //    assert_eq!(EXPECTED, actual);
+    //}
 
-    #[test]
-    fn create_directory() {
-        const EXPECTED: &str = "etc";
-        let mut case = TestCase::new();
-        let mnt_path = case.mnt_path();
-        let dir_path = mnt_path.join(EXPECTED);
+    //#[test]
+    //fn hard_link_then_remove_both() {
+    //    const EXPECTED: &[u8] = b"And the lives we've reclaimed";
+    //    let name1 = OsStr::new("refugee_lyrics.txt");
+    //    let name2 = OsStr::new("rise_against_lyrics.txt");
+    //    let case = TestCase::new();
+    //    let mnt_path = case.mnt_path();
+    //    let path1 = mnt_path.join(name1);
+    //    let path2 = mnt_path.join(name2);
+    //    write(&path1, EXPECTED).expect("write failed");
+
+    //    hard_link(&path1, &path2).expect("hard_link failed");
+    //    remove_file(&path1).expect("remove_file on path1 failed");
+    //    remove_file(&path2).expect("remove_file on path2 failed");
+
+    //    let expected: [&OsStr; 0] = [];
+    //    assert!(file_names(read_dir(&mnt_path).expect("read_dir failed")).eq(expected));
+    //}
 
-        create_dir(&dir_path).expect("create_dir failed");
+    //#[test]
+    //fn set_mode_bits() {
+    //    const EXPECTED: u32 = libc::S_IFREG | 0o777;
+    //    let case = TestCase::new();
+    //    let file_path = case.mnt_path().join("bagobits");
+    //    write(&file_path, []).expect("write failed");
+    //    let original = metadata(&file_path)
+    //        .expect("metadata failed")
+    //        .permissions()
+    //        .mode();
+    //    assert_ne!(EXPECTED, original);
+
+    //    set_permissions(&file_path, Permissions::from_mode(EXPECTED))
+    //        .expect("set_permissions failed");
+
+    //    let actual = metadata(&file_path)
+    //        .expect("metadata failed")
+    //        .permissions()
+    //        .mode();
+    //    assert_eq!(EXPECTED, actual);
+    //}
 
-        let actual = file_names(read_dir(mnt_path).expect("read_dir failed"));
-        assert!(actual.eq([EXPECTED]));
-    }
+    //#[test]
+    //fn create_directory() {
+    //    const EXPECTED: &str = "etc";
+    //    let case = TestCase::new();
+    //    let mnt_path = case.mnt_path();
+    //    let dir_path = mnt_path.join(EXPECTED);
 
-    #[test]
-    fn create_file_under_new_directory() {
-        const DIR_NAME: &str = "etc";
-        const FILE_NAME: &str = "file";
-        let mut case = TestCase::new();
-        let mnt_path = case.mnt_path();
-        let dir_path = mnt_path.join(DIR_NAME);
-        let file_path = dir_path.join(FILE_NAME);
+    //    create_dir(&dir_path).expect("create_dir failed");
 
-        create_dir(&dir_path).expect("create_dir failed");
-        write(&file_path, []).expect("write failed");
+    //    let actual = file_names(read_dir(mnt_path).expect("read_dir failed"));
+    //    assert!(actual.eq([EXPECTED]));
+    //}
 
-        let actual = file_names(read_dir(dir_path).expect("read_dir failed"));
-        assert!(actual.eq([FILE_NAME]));
-    }
+    //#[test]
+    //fn create_file_under_new_directory() {
+    //    const DIR_NAME: &str = "etc";
+    //    const FILE_NAME: &str = "file";
+    //    let case = TestCase::new();
+    //    let mnt_path = case.mnt_path();
+    //    let dir_path = mnt_path.join(DIR_NAME);
+    //    let file_path = dir_path.join(FILE_NAME);
 
-    #[test]
-    fn create_then_remove_directory() {
-        const DIR_NAME: &str = "etc";
-        let mut case = TestCase::new();
-        let mnt_path = case.mnt_path();
-        let dir_path = mnt_path.join(DIR_NAME);
+    //    create_dir(&dir_path).expect("create_dir failed");
+    //    write(&file_path, []).expect("write failed");
 
-        create_dir(&dir_path).expect("create_dir failed");
-        remove_dir(&dir_path).expect("remove_dir failed");
+    //    let actual = file_names(read_dir(dir_path).expect("read_dir failed"));
+    //    assert!(actual.eq([FILE_NAME]));
+    //}
 
-        let actual = file_names(read_dir(&mnt_path).expect("read_dir failed"));
-        const EMPTY: [&str; 0] = [""; 0];
-        assert!(actual.eq(EMPTY));
-    }
+    //#[test]
+    //fn create_then_remove_directory() {
+    //    const DIR_NAME: &str = "etc";
+    //    let case = TestCase::new();
+    //    let mnt_path = case.mnt_path();
+    //    let dir_path = mnt_path.join(DIR_NAME);
 
-    #[test]
-    fn read_only_dir_cant_create_subdir() {
-        const DIR_NAME: &str = "etc";
-        let mut case = TestCase::new();
-        let dir_path = case.mnt_path().join(DIR_NAME);
-        create_dir(&dir_path).expect("create_dir failed");
-        set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444))
-            .expect("set_permissions failed");
+    //    create_dir(&dir_path).expect("create_dir failed");
+    //    remove_dir(&dir_path).expect("remove_dir failed");
 
-        let result = create_dir(dir_path.join("sub"));
+    //    let actual = file_names(read_dir(&mnt_path).expect("read_dir failed"));
+    //    const EMPTY: [&str; 0] = [""; 0];
+    //    assert!(actual.eq(EMPTY));
+    //}
 
-        let err = result.err().expect("create_dir returned `Ok`");
-        let os_err = err.raw_os_error().expect("raw_os_error was empty");
-        assert_eq!(os_err, libc::EACCES);
-    }
+    //#[test]
+    //fn read_only_dir_cant_create_subdir() {
+    //    const DIR_NAME: &str = "etc";
+    //    let case = TestCase::new();
+    //    let dir_path = case.mnt_path().join(DIR_NAME);
+    //    create_dir(&dir_path).expect("create_dir failed");
+    //    set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444))
+    //        .expect("set_permissions failed");
 
-    #[test]
-    fn read_only_dir_cant_remove_subdir() {
-        const DIR_NAME: &str = "etc";
-        let mut case = TestCase::new();
-        let dir_path = case.mnt_path().join(DIR_NAME);
-        let sub_path = dir_path.join("sub");
-        create_dir(&dir_path).expect("create_dir failed");
-        create_dir(&sub_path).expect("create_dir failed");
-        set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444))
-            .expect("set_permissions failed");
-
-        let result = remove_dir(&sub_path);
-
-        let err = result.err().expect("remove_dir returned `Ok`");
-        let os_err = err.raw_os_error().expect("raw_os_error was empty");
-        assert_eq!(os_err, libc::EACCES);
-    }
+    //    let result = create_dir(dir_path.join("sub"));
 
-    #[test]
-    fn rename_file() {
-        const FILE_NAME: &str = "parabola.txt";
-        const EXPECTED: &[u8] = b"We are eternal all this pain is an illusion";
-        let mut case = TestCase::new();
-        let src_path = case.mnt_path().join(FILE_NAME);
-        let dst_path = case.mnt_path().join("parabola_lyrics.txt");
+    //    let err = result.err().expect("create_dir returned `Ok`");
+    //    let os_err = err.raw_os_error().expect("raw_os_error was empty");
+    //    assert_eq!(os_err, libc::EACCES);
+    //}
 
-        write(&src_path, EXPECTED).unwrap();
-        rename(&src_path, &dst_path).unwrap();
+    //#[test]
+    //fn read_only_dir_cant_remove_subdir() {
+    //    const DIR_NAME: &str = "etc";
+    //    let case = TestCase::new();
+    //    let dir_path = case.mnt_path().join(DIR_NAME);
+    //    let sub_path = dir_path.join("sub");
+    //    create_dir(&dir_path).expect("create_dir failed");
+    //    create_dir(&sub_path).expect("create_dir failed");
+    //    set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444))
+    //        .expect("set_permissions failed");
+
+    //    let result = remove_dir(&sub_path);
+
+    //    let err = result.err().expect("remove_dir returned `Ok`");
+    //    let os_err = err.raw_os_error().expect("raw_os_error was empty");
+    //    assert_eq!(os_err, libc::EACCES);
+    //}
 
-        let actual = read(&dst_path).unwrap();
-        assert_eq!(EXPECTED, actual)
-    }
+    //#[test]
+    //fn rename_file() {
+    //    const FILE_NAME: &str = "parabola.txt";
+    //    const EXPECTED: &[u8] = b"We are eternal all this pain is an illusion";
+    //    let case = TestCase::new();
+    //    let src_path = case.mnt_path().join(FILE_NAME);
+    //    let dst_path = case.mnt_path().join("parabola_lyrics.txt");
+
+    //    write(&src_path, EXPECTED).unwrap();
+    //    rename(&src_path, &dst_path).unwrap();
+
+    //    let actual = read(&dst_path).unwrap();
+    //    assert_eq!(EXPECTED, actual)
+    //}
 }

+ 2 - 1
crates/btlib/Cargo.toml

@@ -31,6 +31,7 @@ positioned-io = "0.3.1"
 x509-certificate = { path = "../../../cryptography-rs/x509-certificate" }
 bcder = "0.7.1"
 bytes = "1.3.0"
+safemem = "0.3.3"
 
 [dev-dependencies]
 tempdir = { version = "0.3.7" }
@@ -42,4 +43,4 @@ webpki = "0.22.0"
 
 [[bench]]
 name = "block_benches"
-harness = false
+harness = false

+ 8 - 1
crates/btlib/src/accessor.rs

@@ -1,9 +1,10 @@
 use positioned_io::{ReadAt, Size, WriteAt};
-use std::io::{Read, Seek, SeekFrom, Write};
+use std::io::{self, Read, Seek, SeekFrom, Write};
 
 use crate::{
     sectored_buf::SectoredBuf, BlockMeta, Cursor, Decompose, FlushMeta, MetaAccess, Positioned,
     ReadDual, Result, SecretStream, Sectored, Split, TryCompose, TrySeek, WriteDual,
+    ZeroExtendable,
 };
 
 pub use private::Accessor;
@@ -42,6 +43,12 @@ mod private {
         }
     }
 
+    impl<T: Size + ReadAt + WriteAt + MetaAccess> ZeroExtendable for Accessor<T> {
+        fn zero_extend(&mut self, len: u64) -> io::Result<()> {
+            self.inner.zero_extend(len)
+        }
+    }
+
     impl<T: ReadAt + AsRef<BlockMeta> + Size> Read for Accessor<T> {
         fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
             self.inner.read(buf)

+ 12 - 10
crates/btlib/src/crypto/tpm.rs

@@ -1525,11 +1525,13 @@ impl HasResponseCode for TSS2_RC {
 
 #[cfg(test)]
 mod test {
-    use crate::test_helpers::BtCursor;
-    use swtpm_harness::SwtpmHarness;
-
     use super::*;
 
+    use crate::{
+        test_helpers::BtCursor,
+        error::AnyhowErrorExt,
+    };
+    use swtpm_harness::SwtpmHarness;
     use ctor::ctor;
     use std::{fs::File, io::SeekFrom};
     use tss_esapi::{
@@ -1616,7 +1618,7 @@ mod test {
     /// Tests that a TPM Credential Store can be created when a cookie does not already exist.
     #[test]
     fn tpm_cred_store_new() -> Result<()> {
-        let harness = SwtpmHarness::new()?;
+        let harness = SwtpmHarness::new().bterr()?;
         let cookie_path = harness.dir_path().join("cookie.bin");
         let store = TpmCredStore::new(harness.context()?, cookie_path.to_owned())?;
         let cookie = File::open(&cookie_path)?;
@@ -1630,7 +1632,7 @@ mod test {
 
     #[test]
     fn gen_creds() -> Result<()> {
-        let harness = SwtpmHarness::new()?;
+        let harness = SwtpmHarness::new().bterr()?;
         let cookie_path = harness.dir_path().join("cookie.bin");
         let store = TpmCredStore::new(harness.context()?, cookie_path)?;
         store.gen_node_creds()?;
@@ -1674,7 +1676,7 @@ mod test {
     /// Returns a SwtpmHarness and a TpmCredStore that uses it. Note that the order of the entries
     /// in the returned tuple is significant, as TpmCredStore must be dropped _before_ SwtpmHarness.
     fn test_store() -> Result<(SwtpmHarness, TpmCredStore)> {
-        let harness = SwtpmHarness::new()?;
+        let harness = SwtpmHarness::new().bterr()?;
         let store = TpmCredStore::new(harness.context()?, harness.state_path().to_owned())?;
         Ok((harness, store))
     }
@@ -1720,7 +1722,7 @@ mod test {
 
     #[test]
     fn persistent_handles() -> Result<()> {
-        let harness = SwtpmHarness::new()?;
+        let harness = SwtpmHarness::new().bterr()?;
         let mut context = harness.context()?;
         context.persistent_handles()?;
         Ok(())
@@ -1728,7 +1730,7 @@ mod test {
 
     #[test]
     fn first_free_persistent() -> Result<()> {
-        let harness = SwtpmHarness::new()?;
+        let harness = SwtpmHarness::new().bterr()?;
         let mut context = harness.context()?;
         context.unused_persistent_primary_key()?;
         Ok(())
@@ -1941,7 +1943,7 @@ mod test {
     fn key_export_import() -> Result<()> {
         let auth = Auth::try_from(vec![0u8; 32])?;
 
-        let src_harness = SwtpmHarness::new()?;
+        let src_harness = SwtpmHarness::new().bterr()?;
         let mut src_ctx = src_harness.context()?;
         {
             let session = src_ctx.start_default_auth_session()?;
@@ -1957,7 +1959,7 @@ mod test {
                 .private
         };
 
-        let dest_harness = SwtpmHarness::new()?;
+        let dest_harness = SwtpmHarness::new().bterr()?;
         let mut dest_ctx = dest_harness.context()?;
         {
             let session = dest_ctx.start_default_auth_session()?;

+ 10 - 0
crates/btlib/src/error.rs

@@ -204,6 +204,16 @@ impl<T, E: ::std::error::Error + Send + Sync + 'static> BtErr<T> for ::std::resu
     }
 }
 
+pub trait AnyhowErrorExt<T> {
+    fn bterr(self) -> Result<T>;
+}
+
+impl<T> AnyhowErrorExt<T> for anyhow::Result<T> {
+    fn bterr(self) -> Result<T> {
+        self.map_err(|err| bterr!(err))
+    }
+}
+
 pub trait IoErr<T> {
     /// Maps the error in this result to an `io::Error`.
     fn io_err(self) -> io::Result<T>;

+ 8 - 0
crates/btlib/src/lib.rs

@@ -236,6 +236,14 @@ pub trait WriteDual: Write {
     fn write_from<R: Read>(&mut self, read: R, count: usize) -> io::Result<usize>;
 }
 
+/// Trait for types which can be extended with zero byes.
+pub trait ZeroExtendable {
+    /// Extends this stream with the given number of zero bytes. The position of the stream must
+    /// be unchanged when this method returns successfully. The state of the stream in the case of
+    /// an error is undefined.
+    fn zero_extend(&mut self, num_zeros: u64) -> io::Result<()>;
+}
+
 /// Trait for streams which can efficiently and infallibly return their current position.
 pub trait Positioned {
     /// Returns the position of this stream (byte offset relative to the beginning).

+ 144 - 11
crates/btlib/src/sectored_buf.rs

@@ -1,11 +1,12 @@
 use log::error;
 use positioned_io::Size;
+use safemem::write_bytes;
 use std::io::{self, Read, Seek, SeekFrom, Write};
 
 use crate::{
-    bterr, suppress_err_if_non_zero, BlockError, BlockMeta, BoxInIoErr, Decompose, MetaAccess,
-    Positioned, ReadDual, ReadExt, Result, Sectored, SeekFromExt, SizeExt, Split, TryCompose,
-    TrySeek, WriteDual, EMPTY_SLICE,
+    bterr, error::DisplayErr, suppress_err_if_non_zero, BlockError, BlockMeta, BoxInIoErr,
+    Decompose, MetaAccess, Positioned, ReadDual, ReadExt, Result, Sectored, SeekFromExt, SizeExt,
+    Split, TryCompose, TrySeek, WriteDual, ZeroExtendable, EMPTY_SLICE,
 };
 
 pub use private::SectoredBuf;
@@ -47,7 +48,7 @@ mod private {
         ($self:expr) => {{
             let pos = $self.buf_pos();
             let end = $self.buf_end();
-            if pos == end {
+            if pos == end && $self.pos < $self.len() {
                 match $self.fill_internal_buf() {
                     Ok(nread) => {
                         if nread > 0 {
@@ -204,10 +205,7 @@ mod private {
                 src = &src[sz..];
                 self.dirty = sz > 0;
                 self.pos += sz;
-                self.inner.mut_meta_body().access_secrets(|secrets| {
-                    secrets.size = secrets.size.max(self.pos as u64);
-                    Ok(())
-                })?;
+                Self::update_size(&mut self.inner, self.pos)?;
             }
             Ok(src_len_start - src.len())
         }
@@ -255,6 +253,61 @@ mod private {
         }
     }
 
+    impl<T: Read + Write + Seek + MetaAccess> SectoredBuf<T> {
+        pub fn update_size(inner: &mut T, pos: usize) -> Result<()> {
+            inner.mut_meta_body().access_secrets(|secrets| {
+                secrets.size = secrets.size.max(pos as u64);
+                Ok(())
+            })
+        }
+    }
+
+    impl<T: Read + Write + Seek + MetaAccess> ZeroExtendable for SectoredBuf<T> {
+        fn zero_extend(&mut self, num_zeros: u64) -> io::Result<()> {
+            if num_zeros == 0 {
+                return Ok(());
+            }
+
+            let prev_pos = self.pos;
+            let num_zeros_sz: usize = num_zeros.try_into().display_err()?;
+            self.seek(SeekFrom::End(0))?;
+            let end_pos = self.pos + num_zeros_sz;
+
+            {
+                let start = self.buf_pos();
+                let end = self.buf.len().min(start + num_zeros_sz);
+                write_bytes(&mut self.buf[start..end], 0);
+                self.dirty = self.dirty || end > start;
+                self.pos += end - start;
+                Self::update_size(&mut self.inner, self.pos)?;
+                self.flush()?;
+            }
+
+            if self.pos >= end_pos {
+                self.seek(SeekFrom::Start(prev_pos as u64))?;
+                return Ok(());
+            }
+
+            write_bytes(&mut self.buf, 0);
+            let iters = (end_pos - self.pos) / self.buf.len();
+            for _ in 0..iters {
+                self.dirty = true;
+                self.pos += self.buf.len();
+                Self::update_size(&mut self.inner, self.pos)?;
+                self.flush()?;
+            }
+
+            let remain = (end_pos - self.pos) % self.buf.len();
+            self.pos += remain;
+            self.dirty = remain > 0;
+            Self::update_size(&mut self.inner, self.pos)?;
+            self.flush()?;
+
+            self.seek(SeekFrom::Start(prev_pos as u64))?;
+            Ok(())
+        }
+    }
+
     impl<T: Read + Seek + AsRef<BlockMeta>> Read for SectoredBuf<T> {
         fn read(&mut self, mut dest: &mut [u8]) -> io::Result<usize> {
             if self.pos == self.len() {
@@ -445,9 +498,12 @@ mod private {
 mod tests {
     use super::*;
 
-    use crate::test_helpers::{
-        integer_array, read_check, write_fill, BtCursor, Randomizer, SectoredCursor,
-        SECTOR_SZ_DEFAULT,
+    use crate::{
+        test_helpers::{
+            integer_array, read_check, write_fill, BtCursor, Randomizer, SectoredCursor,
+            SECTOR_SZ_DEFAULT,
+        },
+        Cursor,
     };
 
     fn make_sectored_buf(sect_sz: usize, sect_ct: usize) -> SectoredBuf<SectoredCursor<Vec<u8>>> {
@@ -835,4 +891,81 @@ mod tests {
         assert_eq!(EXPECTED_LEN, nread);
         assert_eq!(&EXPECTED, actual.into_inner().as_slice());
     }
+
+    #[test]
+    fn read_into_reads_nothing_when_at_end() {
+        const SECT_SZ: usize = 8;
+        let mut sectored = SectoredBuf::new()
+            .try_compose(SectoredCursor::new(Vec::new(), SECT_SZ))
+            .unwrap();
+
+        sectored.write([1u8; 6].as_slice()).unwrap();
+        let mut actual = Cursor::new(Vec::new());
+        sectored.read_into(&mut actual, SECT_SZ).unwrap();
+
+        assert_eq!(&[0u8; 0], actual.get_ref().as_slice());
+    }
+
+    #[test]
+    fn zero_extend_less_than_sect_sz() {
+        let mut sectored = SectoredBuf::new()
+            .try_compose(SectoredCursor::new(Vec::new(), 8))
+            .unwrap();
+
+        let written = sectored.write([1u8; 4].as_slice()).unwrap();
+        assert_eq!(4, written);
+        sectored.zero_extend(2).unwrap();
+        sectored.rewind().unwrap();
+        let mut actual = Cursor::new(Vec::new());
+        sectored.read_into(&mut actual, 8).unwrap();
+
+        assert_eq!(&[1, 1, 1, 1, 0, 0], actual.get_ref().as_slice());
+    }
+
+    #[test]
+    fn zero_extend_multiple_sectors() {
+        const SECT_SZ: usize = 8;
+        let mut sectored = SectoredBuf::new()
+            .try_compose(SectoredCursor::new(Vec::new(), SECT_SZ))
+            .unwrap();
+
+        let written = sectored.write([1u8; SECT_SZ / 2].as_slice()).unwrap();
+        assert_eq!(SECT_SZ / 2, written);
+        const EXPECTED_LEN: usize = 3 * SECT_SZ / 2;
+        sectored.rewind().unwrap();
+        // Note that zero_extend is called when the current position is 0. The current position
+        // must not affect zero extension.
+        sectored.zero_extend(EXPECTED_LEN as u64).unwrap();
+        let mut actual = Cursor::new(Vec::new());
+        sectored
+            .read_into(&mut actual, EXPECTED_LEN + SECT_SZ / 2)
+            .unwrap();
+
+        let actual = actual.into_inner();
+        assert_eq!(&[1, 1, 1, 1], &actual[..(SECT_SZ / 2)]);
+        assert_eq!(&[0u8; EXPECTED_LEN], &actual[(SECT_SZ / 2)..]);
+    }
+
+    #[test]
+    fn zero_extend_multiple_sectors_with_remainder() {
+        const SECT_SZ: usize = 8;
+        let mut sectored = SectoredBuf::new()
+            .try_compose(SectoredCursor::new(Vec::new(), SECT_SZ))
+            .unwrap();
+
+        let written = sectored.write([1u8; SECT_SZ / 2].as_slice()).unwrap();
+        assert_eq!(SECT_SZ / 2, written);
+        // Notice that the total length of the inner stream will be 2 * SECT_SZ + 1.
+        const EXPECTED_LEN: usize = 3 * SECT_SZ / 2 + 1;
+        sectored.rewind().unwrap();
+        sectored.zero_extend(EXPECTED_LEN as u64).unwrap();
+        let mut actual = Cursor::new(Vec::new());
+        sectored
+            .read_into(&mut actual, EXPECTED_LEN + SECT_SZ / 2)
+            .unwrap();
+
+        let actual = actual.into_inner();
+        assert_eq!(&[1, 1, 1, 1], &actual[..(SECT_SZ / 2)]);
+        assert_eq!(&[0u8; EXPECTED_LEN], &actual[(SECT_SZ / 2)..]);
+    }
 }

+ 2 - 1
crates/swtpm-harness/Cargo.toml

@@ -10,4 +10,5 @@ dbus = { version = "0.9.6" }
 tempdir = { version = "0.3.7" }
 tss-esapi = { version = "7.1.0", features = ["generate-bindings"] }
 log = { version = "0.4.17" }
-nix = { version = "0.25.0" }
+nix = { version = "0.25.0" }
+anyhow = { version = "1.0.66", features = ["std", "backtrace"] }

+ 7 - 9
crates/swtpm-harness/src/lib.rs

@@ -1,3 +1,4 @@
+use anyhow::anyhow;
 use log::error;
 use nix::{
     sys::signal::{self, Signal},
@@ -42,7 +43,7 @@ impl SwtpmHarness {
         format!("com.intel.tss2.Tabrmd.{port_str}")
     }
 
-    pub fn new() -> io::Result<SwtpmHarness> {
+    pub fn new() -> anyhow::Result<SwtpmHarness> {
         static PORT: AtomicU16 = AtomicU16::new(21901);
         let port = PORT.fetch_add(2, Ordering::SeqCst);
         let ctrl_port = port + 1;
@@ -50,7 +51,7 @@ impl SwtpmHarness {
         let dir_path = dir.path();
         let dir_path_display = dir_path.display();
         let conf_path = dir_path.join("swtpm_setup.conf");
-        let state_path = dir_path.join("state.bt");
+        let state_path = dir_path.join("tpm_cred_store.state");
         let pid_path = dir_path.join("swtpm.pid");
         let dbus_name = Self::dbus_name(port);
         let addr = Self::HOST;
@@ -143,18 +144,15 @@ impl Drop for SwtpmHarness {
 }
 
 trait ExitStatusExt {
-    fn success_or_err(&self) -> io::Result<()>;
+    fn success_or_err(&self) -> anyhow::Result<()>;
 }
 
 impl ExitStatusExt for ExitStatus {
-    fn success_or_err(&self) -> io::Result<()> {
+    fn success_or_err(&self) -> anyhow::Result<()> {
         match self.code() {
             Some(0) => Ok(()),
-            Some(code) => Err(io::Error::new(
-                io::ErrorKind::Other,
-                format!("ExitCode was non-zero: {code}"),
-            )),
-            None => Err(io::Error::new(io::ErrorKind::Other, "ExitCode was None")),
+            Some(code) => Err(anyhow!("ExitCode was non-zero: {code}")),
+            None => Err(anyhow!("ExitCode was None")),
         }
     }
 }