Re-organize authfs directories

* authfs/fd_server     -> android/fd_server
* authfs/service       -> guest/authfs_service
* authfs               -> guest/authfs
* authfs/aidl          -> libs/authfs_aidl_interface
* authfs/tests         -> tests/authfs
* authfs/testdata      -> tests/authfs/testdata

Bug: 352458998
Test: pass TH

Change-Id: I5962d2fafc9f05b240068740ee1b6369406eb1f5
diff --git a/guest/authfs/Android.bp b/guest/authfs/Android.bp
new file mode 100644
index 0000000..b11da3d
--- /dev/null
+++ b/guest/authfs/Android.bp
@@ -0,0 +1,52 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "authfs_defaults",
+    crate_name: "authfs",
+    edition: "2021",
+    srcs: [":authfs_src"],
+    rustlibs: [
+        "authfs_aidl_interface-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libauthfs_fsverity_metadata",
+        "libbinder_rs",
+        "libcfg_if",
+        "libclap",
+        "libfsverity_digests_proto_rust",
+        "libfuse_rust",
+        "libhex",
+        "liblibc",
+        "liblog_rust",
+        "libnix",
+        "libopenssl",
+        "libprotobuf",
+        "librpcbinder_rs",
+        "libthiserror",
+    ],
+    prefer_rlib: true,
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+    defaults: [
+        "crosvm_defaults",
+        "avf_build_flags_rust",
+    ],
+}
+
+filegroup {
+    name: "authfs_src",
+    srcs: [
+        "src/main.rs",
+    ],
+}
+
+rust_binary {
+    name: "authfs",
+    defaults: ["authfs_defaults"],
+    apex_available: ["com.android.virt"],
+}
diff --git a/guest/authfs/TEST_MAPPING b/guest/authfs/TEST_MAPPING
new file mode 100644
index 0000000..62bc18f
--- /dev/null
+++ b/guest/authfs/TEST_MAPPING
@@ -0,0 +1,23 @@
+// When adding or removing tests here, don't forget to amend _all_modules list in
+// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
+{
+  "avf-presubmit": [
+    {
+      "name": "authfs_device_test_src_lib"
+    },
+    {
+      "name": "fd_server.test"
+    },
+    {
+      "name": "open_then_run.test"
+    },
+    {
+      "name": "AuthFsHostTest"
+    }
+  ],
+  "avf-postsubmit": [
+    {
+      "name": "AuthFsBenchmarks"
+    }
+  ]
+}
diff --git a/guest/authfs/src/common.rs b/guest/authfs/src/common.rs
new file mode 100644
index 0000000..6556fde
--- /dev/null
+++ b/guest/authfs/src/common.rs
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// Common block and page size in Linux.
+pub const CHUNK_SIZE: u64 = 4096;
+
+pub fn divide_roundup(dividend: u64, divisor: u64) -> u64 {
+    (dividend + divisor - 1) / divisor
+}
+
+/// Given `offset` and `length`, generates (offset, size) tuples that together form the same length,
+/// and aligned to `alignment`.
+pub struct ChunkedSizeIter {
+    remaining: usize,
+    offset: u64,
+    alignment: usize,
+}
+
+impl ChunkedSizeIter {
+    pub fn new(remaining: usize, offset: u64, alignment: usize) -> Self {
+        ChunkedSizeIter { remaining, offset, alignment }
+    }
+}
+
+impl Iterator for ChunkedSizeIter {
+    type Item = (u64, usize);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.remaining == 0 {
+            return None;
+        }
+        let chunk_data_size = std::cmp::min(
+            self.remaining,
+            self.alignment - (self.offset % self.alignment as u64) as usize,
+        );
+        let retval = (self.offset, chunk_data_size);
+        self.offset += chunk_data_size as u64;
+        self.remaining = self.remaining.saturating_sub(chunk_data_size);
+        Some(retval)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn collect_chunk_read_iter(remaining: usize, offset: u64) -> Vec<(u64, usize)> {
+        ChunkedSizeIter::new(remaining, offset, 4096).collect::<Vec<_>>()
+    }
+
+    #[test]
+    fn test_chunk_read_iter() {
+        assert_eq!(collect_chunk_read_iter(4096, 0), [(0, 4096)]);
+        assert_eq!(collect_chunk_read_iter(8192, 0), [(0, 4096), (4096, 4096)]);
+        assert_eq!(collect_chunk_read_iter(8192, 4096), [(4096, 4096), (8192, 4096)]);
+
+        assert_eq!(
+            collect_chunk_read_iter(16384, 1),
+            [(1, 4095), (4096, 4096), (8192, 4096), (12288, 4096), (16384, 1)]
+        );
+
+        assert_eq!(collect_chunk_read_iter(0, 0), []);
+        assert_eq!(collect_chunk_read_iter(0, 100), []);
+    }
+}
diff --git a/guest/authfs/src/file.rs b/guest/authfs/src/file.rs
new file mode 100644
index 0000000..55c783b
--- /dev/null
+++ b/guest/authfs/src/file.rs
@@ -0,0 +1,114 @@
+mod attr;
+mod dir;
+mod remote_file;
+
+pub use attr::Attr;
+pub use dir::{InMemoryDir, RemoteDirEditor};
+pub use remote_file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
+
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::IVirtFdService;
+use binder::{Status, StatusCode, Strong};
+use rpcbinder::RpcSession;
+use std::convert::TryFrom;
+use std::io;
+use std::path::{Path, MAIN_SEPARATOR};
+
+pub type VirtFdService = Strong<dyn IVirtFdService>;
+pub type VirtFdServiceStatus = Status;
+
+pub type ChunkBuffer = [u8; CHUNK_SIZE as usize];
+
+pub const RPC_SERVICE_PORT: u32 = 3264;
+
+pub fn get_rpc_binder_service(cid: u32) -> io::Result<VirtFdService> {
+    RpcSession::new().setup_vsock_client(cid, RPC_SERVICE_PORT).map_err(|e| match e {
+        StatusCode::BAD_VALUE => {
+            io::Error::new(io::ErrorKind::InvalidInput, "Invalid raw AIBinder")
+        }
+        _ => io::Error::new(
+            io::ErrorKind::AddrNotAvailable,
+            format!("Cannot connect to RPC service: {}", e),
+        ),
+    })
+}
+
+/// A trait for reading data by chunks. Chunks can be read by specifying the chunk index. Only the
+/// last chunk may have incomplete chunk size.
+pub trait ReadByChunk {
+    /// Reads the `chunk_index`-th chunk to a `ChunkBuffer`. Returns the size read, which has to be
+    /// `CHUNK_SIZE` except for the last incomplete chunk. Reading beyond the file size (including
+    /// empty file) should return 0.
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize>;
+}
+
+/// A trait to write a buffer to the destination at a given offset. The implementation does not
+/// necessarily own or maintain the destination state.
+///
+/// NB: The trait is required in a member of `fusefs::AuthFs`, which is required to be Sync and
+/// immutable (this the member).
+pub trait RandomWrite {
+    /// Writes `buf` to the destination at `offset`. Returns the written size, which may not be the
+    /// full buffer.
+    fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>;
+
+    /// Writes the full `buf` to the destination at `offset`.
+    fn write_all_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
+        let mut input_offset = 0;
+        let mut output_offset = offset;
+        while input_offset < buf.len() {
+            let size = self.write_at(&buf[input_offset..], output_offset)?;
+            input_offset += size;
+            output_offset += size as u64;
+        }
+        Ok(())
+    }
+
+    /// Resizes the file to the new size.
+    fn resize(&self, size: u64) -> io::Result<()>;
+}
+
+/// Checks whether the path is a simple file name without any directory separator.
+pub fn validate_basename(path: &Path) -> io::Result<()> {
+    if matches!(path.to_str(), Some(path_str) if !path_str.contains(MAIN_SEPARATOR)) {
+        Ok(())
+    } else {
+        Err(io::Error::from_raw_os_error(libc::EINVAL))
+    }
+}
+
+pub struct EagerChunkReader {
+    buffer: Vec<u8>,
+}
+
+impl EagerChunkReader {
+    pub fn new<F: ReadByChunk>(chunked_file: F, file_size: u64) -> io::Result<EagerChunkReader> {
+        let last_index = divide_roundup(file_size, CHUNK_SIZE);
+        let file_size = usize::try_from(file_size).unwrap();
+        let mut buffer = Vec::with_capacity(file_size);
+        let mut chunk_buffer = [0; CHUNK_SIZE as usize];
+        for index in 0..last_index {
+            let size = chunked_file.read_chunk(index, &mut chunk_buffer)?;
+            buffer.extend_from_slice(&chunk_buffer[..size]);
+        }
+        if buffer.len() < file_size {
+            Err(io::Error::new(
+                io::ErrorKind::InvalidData,
+                format!("Insufficient data size ({} < {})", buffer.len(), file_size),
+            ))
+        } else {
+            Ok(EagerChunkReader { buffer })
+        }
+    }
+}
+
+impl ReadByChunk for EagerChunkReader {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        if let Some(chunk) = &self.buffer.chunks(CHUNK_SIZE as usize).nth(chunk_index as usize) {
+            buf[..chunk.len()].copy_from_slice(chunk);
+            Ok(chunk.len())
+        } else {
+            Ok(0) // Read beyond EOF is normal
+        }
+    }
+}
diff --git a/guest/authfs/src/file/attr.rs b/guest/authfs/src/file/attr.rs
new file mode 100644
index 0000000..48084aa
--- /dev/null
+++ b/guest/authfs/src/file/attr.rs
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use log::error;
+use nix::sys::stat::{mode_t, Mode, SFlag};
+use std::io;
+
+use super::VirtFdService;
+
+/// Default/assumed mode of files not created by authfs.
+///
+/// For files that are given to authfs as FDs (i.e. not created through authfs), their mode is
+/// unknown (or untrusted) until it is ever set. The default mode is just to make it
+/// readable/writable to VFS. When the mode is set, the value on fd_server is supposed to become
+/// consistent.
+const DEFAULT_FILE_MODE: Mode =
+    Mode::from_bits_truncate(Mode::S_IRUSR.bits() | Mode::S_IWUSR.bits());
+
+/// Default/assumed mode of directories not created by authfs.
+///
+/// See above.
+const DEFAULT_DIR_MODE: Mode = Mode::S_IRWXU;
+
+/// `Attr` maintains the local truth for attributes (e.g. mode and type) while allowing setting the
+/// remote attribute for the file description.
+pub struct Attr {
+    service: VirtFdService,
+    mode: Mode,
+    remote_fd: i32,
+    is_dir: bool,
+}
+
+impl Attr {
+    pub fn new_file(service: VirtFdService, remote_fd: i32) -> Attr {
+        Attr { service, mode: DEFAULT_FILE_MODE, remote_fd, is_dir: false }
+    }
+
+    pub fn new_dir(service: VirtFdService, remote_fd: i32) -> Attr {
+        Attr { service, mode: DEFAULT_DIR_MODE, remote_fd, is_dir: true }
+    }
+
+    pub fn new_file_with_mode(service: VirtFdService, remote_fd: i32, mode: mode_t) -> Attr {
+        Attr { service, mode: Mode::from_bits_truncate(mode), remote_fd, is_dir: false }
+    }
+
+    pub fn new_dir_with_mode(service: VirtFdService, remote_fd: i32, mode: mode_t) -> Attr {
+        Attr { service, mode: Mode::from_bits_truncate(mode), remote_fd, is_dir: true }
+    }
+
+    pub fn mode(&self) -> u32 {
+        self.mode.bits()
+    }
+
+    /// Sets the file mode.
+    ///
+    /// In addition to the actual file mode, `encoded_mode` also contains information of the file
+    /// type.
+    pub fn set_mode(&mut self, encoded_mode: u32) -> io::Result<()> {
+        let new_sflag = SFlag::from_bits_truncate(encoded_mode);
+        let new_mode = Mode::from_bits_truncate(encoded_mode);
+
+        let type_flag = if self.is_dir { SFlag::S_IFDIR } else { SFlag::S_IFREG };
+        if !type_flag.contains(new_sflag) {
+            return Err(io::Error::from_raw_os_error(libc::EINVAL));
+        }
+
+        // Request for update only if changing.
+        if new_mode != self.mode {
+            self.service.chmod(self.remote_fd, new_mode.bits() as i32).map_err(|e| {
+                error!(
+                    "Failed to chmod (fd: {}, mode: {:o}) on fd_server: {:?}",
+                    self.remote_fd, new_mode, e
+                );
+                io::Error::from_raw_os_error(libc::EIO)
+            })?;
+            self.mode = new_mode;
+        }
+        Ok(())
+    }
+}
diff --git a/guest/authfs/src/file/dir.rs b/guest/authfs/src/file/dir.rs
new file mode 100644
index 0000000..5d2ec9f
--- /dev/null
+++ b/guest/authfs/src/file/dir.rs
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use log::warn;
+use nix::sys::stat::Mode;
+use std::collections::{hash_map, HashMap};
+use std::ffi::{CString, OsString};
+use std::io;
+use std::os::unix::ffi::OsStringExt;
+use std::path::{Path, PathBuf};
+
+use super::attr::Attr;
+use super::remote_file::RemoteFileEditor;
+use super::{validate_basename, VirtFdService, VirtFdServiceStatus};
+use crate::fsverity::VerifiedFileEditor;
+use crate::fusefs::{AuthFsDirEntry, Inode};
+
+const MAX_ENTRIES: u16 = 1000; // Arbitrary limit
+
+struct InodeInfo {
+    inode: Inode,
+
+    // This information is duplicated since it is also available in `AuthFs::inode_table` via the
+    // type system. But it makes it simple to deal with deletion, where otherwise we need to get a
+    // mutable parent directory in the table, and query the table for directory/file type checking
+    // at the same time.
+    is_dir: bool,
+}
+
+/// A remote directory backed by a remote directory FD, where the provider/fd_server is not
+/// trusted.
+///
+/// The directory is assumed empty initially without the trust to the storage. Functionally, when
+/// the backing storage is not clean, the fd_server can fail to create a file or directory when
+/// there is name collision. From RemoteDirEditor's perspective of security, the creation failure
+/// is just one of possible errors that can happen, and what matters is RemoteDirEditor maintains
+/// the integrity itself.
+///
+/// When new files are created through RemoteDirEditor, the file integrity are maintained within the
+/// VM. Similarly, integrity (namely the list of entries) of the directory, or new directories
+/// created within such a directory, are also maintained within the VM. A compromised fd_server or
+/// malicious client can't affect the view to the files and directories within such a directory in
+/// the VM.
+pub struct RemoteDirEditor {
+    service: VirtFdService,
+    remote_dir_fd: i32,
+
+    /// Mapping of entry names to the corresponding inode. The actual file/directory is stored in
+    /// the global pool in fusefs.
+    entries: HashMap<PathBuf, InodeInfo>,
+}
+
+impl RemoteDirEditor {
+    pub fn new(service: VirtFdService, remote_dir_fd: i32) -> Self {
+        RemoteDirEditor { service, remote_dir_fd, entries: HashMap::new() }
+    }
+
+    /// Returns the number of entries created.
+    pub fn number_of_entries(&self) -> u16 {
+        self.entries.len() as u16 // limited to MAX_ENTRIES
+    }
+
+    /// Creates a remote file named `basename` with corresponding `inode` at the current directory.
+    pub fn create_file(
+        &mut self,
+        basename: &Path,
+        inode: Inode,
+        mode: libc::mode_t,
+    ) -> io::Result<(VerifiedFileEditor<RemoteFileEditor>, Attr)> {
+        let mode = self.validate_arguments(basename, mode)?;
+        let basename_str =
+            basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+        let new_fd = self
+            .service
+            .createFileInDirectory(self.remote_dir_fd, basename_str, mode as i32)
+            .map_err(into_io_error)?;
+
+        let new_remote_file =
+            VerifiedFileEditor::new(RemoteFileEditor::new(self.service.clone(), new_fd));
+        self.entries.insert(basename.to_path_buf(), InodeInfo { inode, is_dir: false });
+        let new_attr = Attr::new_file_with_mode(self.service.clone(), new_fd, mode);
+        Ok((new_remote_file, new_attr))
+    }
+
+    /// Creates a remote directory named `basename` with corresponding `inode` at the current
+    /// directory.
+    pub fn mkdir(
+        &mut self,
+        basename: &Path,
+        inode: Inode,
+        mode: libc::mode_t,
+    ) -> io::Result<(RemoteDirEditor, Attr)> {
+        let mode = self.validate_arguments(basename, mode)?;
+        let basename_str =
+            basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+        let new_fd = self
+            .service
+            .createDirectoryInDirectory(self.remote_dir_fd, basename_str, mode as i32)
+            .map_err(into_io_error)?;
+
+        let new_remote_dir = RemoteDirEditor::new(self.service.clone(), new_fd);
+        self.entries.insert(basename.to_path_buf(), InodeInfo { inode, is_dir: true });
+        let new_attr = Attr::new_dir_with_mode(self.service.clone(), new_fd, mode);
+        Ok((new_remote_dir, new_attr))
+    }
+
+    /// Deletes a file
+    pub fn delete_file(&mut self, basename: &Path) -> io::Result<Inode> {
+        let inode = self.force_delete_entry(basename, /* expect_dir */ false)?;
+
+        let basename_str =
+            basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+        if let Err(e) = self.service.deleteFile(self.remote_dir_fd, basename_str) {
+            // Ignore the error to honor the local state.
+            warn!("Deletion on the host is reportedly failed: {:?}", e);
+        }
+        Ok(inode)
+    }
+
+    /// Forces to delete a directory. The caller must only call if `basename` is a directory and
+    /// empty.
+    pub fn force_delete_directory(&mut self, basename: &Path) -> io::Result<Inode> {
+        let inode = self.force_delete_entry(basename, /* expect_dir */ true)?;
+
+        let basename_str =
+            basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+        if let Err(e) = self.service.deleteDirectory(self.remote_dir_fd, basename_str) {
+            // Ignore the error to honor the local state.
+            warn!("Deletion on the host is reportedly failed: {:?}", e);
+        }
+        Ok(inode)
+    }
+
+    /// Returns the inode number of a file or directory named `name` previously created through
+    /// `RemoteDirEditor`.
+    pub fn find_inode(&self, name: &Path) -> io::Result<Inode> {
+        self.entries
+            .get(name)
+            .map(|entry| entry.inode)
+            .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
+    }
+
+    /// Returns whether the directory has an entry of the given name.
+    pub fn has_entry(&self, name: &Path) -> bool {
+        self.entries.contains_key(name)
+    }
+
+    pub fn retrieve_entries(&self) -> io::Result<Vec<AuthFsDirEntry>> {
+        self.entries
+            .iter()
+            .map(|(name, InodeInfo { inode, is_dir })| {
+                Ok(AuthFsDirEntry { inode: *inode, name: path_to_cstring(name)?, is_dir: *is_dir })
+            })
+            .collect::<io::Result<Vec<_>>>()
+    }
+
+    fn force_delete_entry(&mut self, basename: &Path, expect_dir: bool) -> io::Result<Inode> {
+        // Kernel should only give us a basename.
+        debug_assert!(validate_basename(basename).is_ok());
+
+        if let Some(entry) = self.entries.get(basename) {
+            match (expect_dir, entry.is_dir) {
+                (true, false) => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+                (false, true) => Err(io::Error::from_raw_os_error(libc::EISDIR)),
+                _ => {
+                    let inode = entry.inode;
+                    let _ = self.entries.remove(basename);
+                    Ok(inode)
+                }
+            }
+        } else {
+            Err(io::Error::from_raw_os_error(libc::ENOENT))
+        }
+    }
+
+    fn validate_arguments(&self, basename: &Path, mode: u32) -> io::Result<u32> {
+        // Kernel should only give us a basename.
+        debug_assert!(validate_basename(basename).is_ok());
+
+        if self.entries.contains_key(basename) {
+            return Err(io::Error::from_raw_os_error(libc::EEXIST));
+        }
+
+        if self.entries.len() >= MAX_ENTRIES.into() {
+            return Err(io::Error::from_raw_os_error(libc::EMLINK));
+        }
+
+        Ok(Mode::from_bits_truncate(mode).bits())
+    }
+}
+
+/// An in-memory directory representation of a directory structure.
+pub struct InMemoryDir(HashMap<PathBuf, InodeInfo>);
+
+impl InMemoryDir {
+    /// Creates an empty instance of `InMemoryDir`.
+    pub fn new() -> Self {
+        // Hash map is empty since "." and ".." are excluded in entries.
+        InMemoryDir(HashMap::new())
+    }
+
+    /// Returns the number of entries in the directory (not including "." and "..").
+    pub fn number_of_entries(&self) -> u16 {
+        self.0.len() as u16 // limited to MAX_ENTRIES
+    }
+
+    /// Adds a directory name and its inode number to the directory. Fails if already exists. The
+    /// caller is responsible for ensure the inode uniqueness.
+    pub fn add_dir(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+        self.add_entry(basename, InodeInfo { inode, is_dir: true })
+    }
+
+    /// Adds a file name and its inode number to the directory. Fails if already exists. The
+    /// caller is responsible for ensure the inode uniqueness.
+    pub fn add_file(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+        self.add_entry(basename, InodeInfo { inode, is_dir: false })
+    }
+
+    fn add_entry(&mut self, basename: &Path, dir_entry: InodeInfo) -> io::Result<()> {
+        validate_basename(basename)?;
+        if self.0.len() >= MAX_ENTRIES.into() {
+            return Err(io::Error::from_raw_os_error(libc::EMLINK));
+        }
+
+        if let hash_map::Entry::Vacant(entry) = self.0.entry(basename.to_path_buf()) {
+            entry.insert(dir_entry);
+            Ok(())
+        } else {
+            Err(io::Error::from_raw_os_error(libc::EEXIST))
+        }
+    }
+
+    /// Looks up an entry inode by name. `None` if not found.
+    pub fn lookup_inode(&self, basename: &Path) -> Option<Inode> {
+        self.0.get(basename).map(|entry| entry.inode)
+    }
+
+    pub fn retrieve_entries(&self) -> io::Result<Vec<AuthFsDirEntry>> {
+        self.0
+            .iter()
+            .map(|(name, InodeInfo { inode, is_dir })| {
+                Ok(AuthFsDirEntry { inode: *inode, name: path_to_cstring(name)?, is_dir: *is_dir })
+            })
+            .collect::<io::Result<Vec<_>>>()
+    }
+}
+
+fn path_to_cstring(path: &Path) -> io::Result<CString> {
+    let bytes = OsString::from(path).into_vec();
+    CString::new(bytes).map_err(|_| io::Error::from_raw_os_error(libc::EILSEQ))
+}
+
+fn into_io_error(e: VirtFdServiceStatus) -> io::Error {
+    let maybe_errno = e.service_specific_error();
+    if maybe_errno > 0 {
+        io::Error::from_raw_os_error(maybe_errno)
+    } else {
+        io::Error::new(io::ErrorKind::Other, e.get_description())
+    }
+}
diff --git a/guest/authfs/src/file/remote_file.rs b/guest/authfs/src/file/remote_file.rs
new file mode 100644
index 0000000..4c112bd
--- /dev/null
+++ b/guest/authfs/src/file/remote_file.rs
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use std::cmp::min;
+use std::convert::TryFrom;
+use std::io;
+use std::path::Path;
+
+use super::{ChunkBuffer, RandomWrite, ReadByChunk, VirtFdService};
+use crate::common::CHUNK_SIZE;
+
+fn remote_read_chunk(
+    service: &VirtFdService,
+    remote_fd: i32,
+    chunk_index: u64,
+    buf: &mut ChunkBuffer,
+) -> io::Result<usize> {
+    let offset = i64::try_from(chunk_index * CHUNK_SIZE)
+        .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+
+    let chunk = service
+        .readFile(remote_fd, offset, buf.len() as i32)
+        .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+    let size = min(buf.len(), chunk.len());
+    buf[..size].copy_from_slice(&chunk[..size]);
+    Ok(size)
+}
+
+pub struct RemoteFileReader {
+    service: VirtFdService,
+    file_fd: i32,
+}
+
+impl RemoteFileReader {
+    pub fn new(service: VirtFdService, file_fd: i32) -> Self {
+        RemoteFileReader { service, file_fd }
+    }
+
+    pub fn new_by_path(
+        service: VirtFdService,
+        dir_fd: i32,
+        related_path: &Path,
+    ) -> io::Result<Self> {
+        let file_fd =
+            service.openFileInDirectory(dir_fd, related_path.to_str().unwrap()).map_err(|e| {
+                io::Error::new(
+                    io::ErrorKind::Other,
+                    format!(
+                        "Failed to create a remote file reader by path {}: {}",
+                        related_path.display(),
+                        e.get_description()
+                    ),
+                )
+            })?;
+        Ok(RemoteFileReader { service, file_fd })
+    }
+
+    pub fn get_remote_fd(&self) -> i32 {
+        self.file_fd
+    }
+}
+
+impl ReadByChunk for RemoteFileReader {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        remote_read_chunk(&self.service, self.file_fd, chunk_index, buf)
+    }
+}
+
+pub struct RemoteMerkleTreeReader {
+    service: VirtFdService,
+    file_fd: i32,
+}
+
+impl RemoteMerkleTreeReader {
+    pub fn new(service: VirtFdService, file_fd: i32) -> Self {
+        RemoteMerkleTreeReader { service, file_fd }
+    }
+}
+
+impl ReadByChunk for RemoteMerkleTreeReader {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        let offset = i64::try_from(chunk_index * CHUNK_SIZE)
+            .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+
+        let chunk = self
+            .service
+            .readFsverityMerkleTree(self.file_fd, offset, buf.len() as i32)
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+        let size = min(buf.len(), chunk.len());
+        buf[..size].copy_from_slice(&chunk[..size]);
+        Ok(size)
+    }
+}
+
+pub struct RemoteFileEditor {
+    service: VirtFdService,
+    file_fd: i32,
+}
+
+impl RemoteFileEditor {
+    pub fn new(service: VirtFdService, file_fd: i32) -> Self {
+        RemoteFileEditor { service, file_fd }
+    }
+}
+
+impl RandomWrite for RemoteFileEditor {
+    fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+        let offset =
+            i64::try_from(offset).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+        let size = self
+            .service
+            .writeFile(self.file_fd, buf, offset)
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+        Ok(size as usize) // within range because size is supposed to <= buf.len(), which is a usize
+    }
+
+    fn resize(&self, size: u64) -> io::Result<()> {
+        let size =
+            i64::try_from(size).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+        self.service
+            .resize(self.file_fd, size)
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+        Ok(())
+    }
+}
+
+impl ReadByChunk for RemoteFileEditor {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        remote_read_chunk(&self.service, self.file_fd, chunk_index, buf)
+    }
+}
diff --git a/guest/authfs/src/fsstat.rs b/guest/authfs/src/fsstat.rs
new file mode 100644
index 0000000..81eaca1
--- /dev/null
+++ b/guest/authfs/src/fsstat.rs
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use log::error;
+use std::convert::TryInto;
+use std::io;
+
+use crate::file::VirtFdService;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::FsStat::FsStat;
+
+/// Relevant/interesting stats of a remote filesystem.
+pub struct RemoteFsStats {
+    /// Block size of the filesystem
+    pub block_size: u64,
+    /// Fragment size of the filesystem
+    pub fragment_size: u64,
+    /// Number of blocks in the filesystem
+    pub block_numbers: u64,
+    /// Number of free blocks
+    pub block_available: u64,
+    /// Number of free inodes
+    pub inodes_available: u64,
+    /// Maximum filename length
+    pub max_filename: u64,
+}
+
+pub struct RemoteFsStatsReader {
+    service: VirtFdService,
+}
+
+impl RemoteFsStatsReader {
+    pub fn new(service: VirtFdService) -> Self {
+        Self { service }
+    }
+
+    pub fn statfs(&self) -> io::Result<RemoteFsStats> {
+        let st = self.service.statfs().map_err(|e| {
+            error!("Failed to call statfs on fd_server: {:?}", e);
+            io::Error::from_raw_os_error(libc::EIO)
+        })?;
+        try_into_remote_fs_stats(st).map_err(|_| {
+            error!("Received invalid stats from fd_server");
+            io::Error::from_raw_os_error(libc::EIO)
+        })
+    }
+}
+
+fn try_into_remote_fs_stats(st: FsStat) -> Result<RemoteFsStats, std::num::TryFromIntError> {
+    Ok(RemoteFsStats {
+        block_size: st.blockSize.try_into()?,
+        fragment_size: st.fragmentSize.try_into()?,
+        block_numbers: st.blockNumbers.try_into()?,
+        block_available: st.blockAvailable.try_into()?,
+        inodes_available: st.inodesAvailable.try_into()?,
+        max_filename: st.maxFilename.try_into()?,
+    })
+}
diff --git a/guest/authfs/src/fsverity.rs b/guest/authfs/src/fsverity.rs
new file mode 100644
index 0000000..61ae928
--- /dev/null
+++ b/guest/authfs/src/fsverity.rs
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+mod builder;
+mod common;
+mod editor;
+mod sys;
+mod verifier;
+
+pub use common::merkle_tree_size;
+pub use editor::VerifiedFileEditor;
+pub use verifier::VerifiedFileReader;
diff --git a/guest/authfs/src/fsverity/builder.rs b/guest/authfs/src/fsverity/builder.rs
new file mode 100644
index 0000000..6d724ca
--- /dev/null
+++ b/guest/authfs/src/fsverity/builder.rs
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use super::common::{
+    build_fsverity_digest, merkle_tree_height, FsverityError, Sha256Hash, SHA256_HASH_SIZE,
+};
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use openssl::sha::Sha256;
+
+const HASH_SIZE: usize = SHA256_HASH_SIZE;
+const HASH_PER_PAGE: usize = CHUNK_SIZE as usize / HASH_SIZE;
+
+const HASH_OF_4096_ZEROS: Sha256Hash = [
+    0xad, 0x7f, 0xac, 0xb2, 0x58, 0x6f, 0xc6, 0xe9, 0x66, 0xc0, 0x04, 0xd7, 0xd1, 0xd1, 0x6b, 0x02,
+    0x4f, 0x58, 0x05, 0xff, 0x7c, 0xb4, 0x7c, 0x7a, 0x85, 0xda, 0xbd, 0x8b, 0x48, 0x89, 0x2c, 0xa7,
+];
+
+/// MerkleLeaves can be used by the class' customer for bookkeeping integrity data for their bytes.
+/// It can also be used to generate the standard fs-verity digest for the source data.
+///
+/// It's in-memory because for the initial use cases, we don't need to read back an existing file,
+/// and only need to deal with new files. Also, considering that the output file won't be large at
+/// the moment, it is sufficient to simply keep the Merkle tree in memory in the trusted world. To
+/// further simplify the initial implementation, we only need to keep the leaf nodes in memory, and
+/// generate the tree / root hash when requested.
+pub struct MerkleLeaves {
+    leaves: Vec<Sha256Hash>,
+    file_size: u64,
+}
+
+fn hash_all_pages(source: &[Sha256Hash]) -> Vec<Sha256Hash> {
+    source
+        .chunks(HASH_PER_PAGE)
+        .map(|chunk| {
+            let padding_bytes = (HASH_PER_PAGE - chunk.len()) * HASH_SIZE;
+            let mut ctx = Sha256::new();
+            for data in chunk {
+                ctx.update(data.as_ref());
+            }
+            ctx.update(&vec![0u8; padding_bytes]);
+            ctx.finish()
+        })
+        .collect()
+}
+
+impl MerkleLeaves {
+    /// Creates a `MerkleLeaves` instance with empty data.
+    pub fn new() -> Self {
+        Self { leaves: Vec::new(), file_size: 0 }
+    }
+
+    /// Gets size of the file represented by `MerkleLeaves`.
+    pub fn file_size(&self) -> u64 {
+        self.file_size
+    }
+
+    /// Grows the `MerkleLeaves` to the new file size. To keep the consistency, if any new leaves
+    /// are added, the leaves/hashes are as if the extended content is all zero.
+    ///
+    /// However, when the change shrinks the leaf number, `MerkleLeaves` does not know if the hash
+    /// of the last chunk has changed, or what the new value should be. As the result, it is up to
+    /// the caller to fix the last leaf if needed.
+    pub fn resize(&mut self, new_file_size: usize) {
+        let new_file_size = new_file_size as u64;
+        let leaves_size = divide_roundup(new_file_size, CHUNK_SIZE);
+        self.leaves.resize(leaves_size as usize, HASH_OF_4096_ZEROS);
+        self.file_size = new_file_size;
+    }
+
+    /// Updates the hash of the `index`-th leaf, and increase the size to `size_at_least` if the
+    /// current size is smaller.
+    pub fn update_hash(&mut self, index: usize, hash: &Sha256Hash, size_at_least: u64) {
+        // +1 since index is zero-based.
+        if self.leaves.len() < index + 1 {
+            // When resizing, fill in hash of zeros by default. This makes it easy to handle holes
+            // in a file.
+            self.leaves.resize(index + 1, HASH_OF_4096_ZEROS);
+        }
+        self.leaves[index].clone_from_slice(hash);
+
+        if size_at_least > self.file_size {
+            self.file_size = size_at_least;
+        }
+    }
+
+    /// Returns whether `index` is within the bound of leaves.
+    pub fn is_index_valid(&self, index: usize) -> bool {
+        index < self.leaves.len()
+    }
+
+    /// Returns whether the `index`-th hash is consistent to `hash`.
+    pub fn is_consistent(&self, index: usize, hash: &Sha256Hash) -> bool {
+        if let Some(element) = self.leaves.get(index) {
+            element == hash
+        } else {
+            false
+        }
+    }
+
+    fn calculate_root_hash(&self) -> Result<Sha256Hash, FsverityError> {
+        match self.leaves.len() {
+            // Special cases per fs-verity digest definition.
+            0 => {
+                debug_assert_eq!(self.file_size, 0);
+                Ok([0u8; HASH_SIZE])
+            }
+            1 => {
+                debug_assert!(self.file_size <= CHUNK_SIZE && self.file_size > 0);
+                Ok(self.leaves[0])
+            }
+            n => {
+                debug_assert_eq!((self.file_size - 1) / CHUNK_SIZE, n as u64);
+                let size_for_equivalent = n as u64 * CHUNK_SIZE;
+                let level = merkle_tree_height(size_for_equivalent).unwrap(); // safe since n > 0
+
+                // `leaves` is owned and can't be the initial state below. Here we manually hash it
+                // first to avoid a copy and to get the type right.
+                let second_level = hash_all_pages(&self.leaves);
+                let hashes = (1..=level).fold(second_level, |source, _| hash_all_pages(&source));
+                if hashes.len() != 1 {
+                    Err(FsverityError::InvalidState)
+                } else {
+                    Ok(hashes.into_iter().next().unwrap())
+                }
+            }
+        }
+    }
+
+    /// Returns the fs-verity digest based on the current tree and file size.
+    pub fn calculate_fsverity_digest(&self) -> Result<Sha256Hash, FsverityError> {
+        let root_hash = self.calculate_root_hash()?;
+        Ok(build_fsverity_digest(&root_hash, self.file_size))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    // Test data below can be generated by:
+    //  $ perl -e 'print "\x{00}" x 6000' > foo
+    //  $ perl -e 'print "\x{01}" x 5000' >> foo
+    //  $ fsverity digest foo
+    use super::*;
+    use anyhow::Result;
+    use openssl::sha::sha256;
+
+    #[test]
+    fn merkle_tree_empty_file() -> Result<()> {
+        assert_eq!(
+            hex::decode("3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95")?,
+            generate_fsverity_digest_sequentially(&Vec::new())?
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn merkle_tree_file_size_less_than_or_equal_to_4k() -> Result<()> {
+        // Test a file that contains 4096 '\01's.
+        assert_eq!(
+            hex::decode("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")?,
+            generate_fsverity_digest_sequentially(&vec![1; 4096])?
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn merkle_tree_more_sizes() -> Result<()> {
+        // Test files that contains >4096 '\01's.
+
+        assert_eq!(
+            hex::decode("2901b849fda2d91e3929524561c4a47e77bb64734319759507b2029f18b9cc52")?,
+            generate_fsverity_digest_sequentially(&vec![1; 4097])?
+        );
+
+        assert_eq!(
+            hex::decode("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")?,
+            generate_fsverity_digest_sequentially(&vec![1; 8192])?
+        );
+
+        // Test with max size that still fits in 2 levels.
+        assert_eq!(
+            hex::decode("26b7c190a34e19f420808ee7ec233b09fa6c34543b5a9d2950530114c205d14f")?,
+            generate_fsverity_digest_sequentially(&vec![1; 524288])?
+        );
+
+        // Test with data that requires 3 levels.
+        assert_eq!(
+            hex::decode("316835d9be1c95b5cd55d07ae7965d651689efad186e26cbf680e40b683a3262")?,
+            generate_fsverity_digest_sequentially(&vec![1; 524289])?
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn merkle_tree_non_sequential() -> Result<()> {
+        let mut tree = MerkleLeaves::new();
+        let hash = sha256(&vec![1u8; CHUNK_SIZE as usize]);
+
+        // Update hashes of 4 1-blocks.
+        tree.update_hash(1, &hash, CHUNK_SIZE * 2);
+        tree.update_hash(3, &hash, CHUNK_SIZE * 4);
+        tree.update_hash(0, &hash, CHUNK_SIZE);
+        tree.update_hash(2, &hash, CHUNK_SIZE * 3);
+
+        assert_eq!(
+            hex::decode("7d3c0d2e1dc54230b20ed875f5f3a4bd3f9873df601936b3ca8127d4db3548f3")?,
+            tree.calculate_fsverity_digest()?
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn merkle_tree_grow_leaves() -> Result<()> {
+        let mut tree = MerkleLeaves::new();
+        tree.update_hash(0, &[42; HASH_SIZE], CHUNK_SIZE); // fake hash of a CHUNK_SIZE block
+
+        // Grows the leaves
+        tree.resize(4096 * 3 - 100);
+
+        assert!(tree.is_index_valid(0));
+        assert!(tree.is_index_valid(1));
+        assert!(tree.is_index_valid(2));
+        assert!(!tree.is_index_valid(3));
+        assert!(tree.is_consistent(1, &HASH_OF_4096_ZEROS));
+        assert!(tree.is_consistent(2, &HASH_OF_4096_ZEROS));
+        Ok(())
+    }
+
+    #[test]
+    fn merkle_tree_shrink_leaves() -> Result<()> {
+        let mut tree = MerkleLeaves::new();
+        tree.update_hash(0, &[42; HASH_SIZE], CHUNK_SIZE);
+        tree.update_hash(0, &[42; HASH_SIZE], CHUNK_SIZE * 3);
+
+        // Shrink the leaves
+        tree.resize(CHUNK_SIZE as usize * 2 - 100);
+
+        assert!(tree.is_index_valid(0));
+        assert!(tree.is_index_valid(1));
+        assert!(!tree.is_index_valid(2));
+        // The second chunk is a hole and full of zero. When shrunk, with zero padding, the hash
+        // happens to be consistent to a full-zero chunk.
+        assert!(tree.is_consistent(1, &HASH_OF_4096_ZEROS));
+        Ok(())
+    }
+
+    fn generate_fsverity_digest_sequentially(test_data: &[u8]) -> Result<Sha256Hash> {
+        let mut tree = MerkleLeaves::new();
+        for (index, chunk) in test_data.chunks(CHUNK_SIZE as usize).enumerate() {
+            let mut ctx = Sha256::new();
+            ctx.update(chunk);
+            ctx.update(&vec![0u8; CHUNK_SIZE as usize - chunk.len()]);
+            let hash = ctx.finish();
+
+            tree.update_hash(index, &hash, CHUNK_SIZE * index as u64 + chunk.len() as u64);
+        }
+        Ok(tree.calculate_fsverity_digest()?)
+    }
+}
diff --git a/guest/authfs/src/fsverity/common.rs b/guest/authfs/src/fsverity/common.rs
new file mode 100644
index 0000000..cb268ef
--- /dev/null
+++ b/guest/authfs/src/fsverity/common.rs
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use std::io;
+
+use thiserror::Error;
+
+use super::sys::{FS_VERITY_HASH_ALG_SHA256, FS_VERITY_LOG_BLOCKSIZE, FS_VERITY_VERSION};
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use openssl::sha::Sha256;
+
+/// Output size of SHA-256 in bytes.
+pub const SHA256_HASH_SIZE: usize = 32;
+
+/// A SHA-256 hash.
+pub type Sha256Hash = [u8; SHA256_HASH_SIZE];
+
+#[derive(Error, Debug)]
+pub enum FsverityError {
+    #[error("Invalid digest")]
+    InvalidDigest,
+    #[error("Insufficient data, only got {0}")]
+    InsufficientData(usize),
+    #[error("Cannot verify a block")]
+    CannotVerify,
+    #[error("I/O error")]
+    Io(#[from] io::Error),
+    #[error("Invalid state")]
+    InvalidState,
+}
+
+fn log128_ceil(num: u64) -> Option<u64> {
+    match num {
+        0 => None,
+        n => Some(divide_roundup(64 - (n - 1).leading_zeros() as u64, 7)),
+    }
+}
+
+/// Return the Merkle tree height for our tree configuration, or None if the size is 0.
+pub fn merkle_tree_height(data_size: u64) -> Option<u64> {
+    let hashes_per_node = CHUNK_SIZE / SHA256_HASH_SIZE as u64;
+    let hash_pages = divide_roundup(data_size, hashes_per_node * CHUNK_SIZE);
+    log128_ceil(hash_pages)
+}
+
+/// Returns the size of Merkle tree for `data_size` bytes amount of data.
+pub fn merkle_tree_size(mut data_size: u64) -> u64 {
+    let mut total = 0;
+    while data_size > CHUNK_SIZE {
+        let hash_size = divide_roundup(data_size, CHUNK_SIZE) * SHA256_HASH_SIZE as u64;
+        let hash_storage_size = divide_roundup(hash_size, CHUNK_SIZE) * CHUNK_SIZE;
+        total += hash_storage_size;
+        data_size = hash_storage_size;
+    }
+    total
+}
+
+pub fn build_fsverity_digest(root_hash: &Sha256Hash, file_size: u64) -> Sha256Hash {
+    // Little-endian byte representation of fsverity_descriptor from linux/fsverity.h
+    // Not FFI-ed as it seems easier to deal with the raw bytes manually.
+    let mut hash = Sha256::new();
+    hash.update(&FS_VERITY_VERSION.to_le_bytes()); // version
+    hash.update(&FS_VERITY_HASH_ALG_SHA256.to_le_bytes()); // hash_algorithm
+    hash.update(&FS_VERITY_LOG_BLOCKSIZE.to_le_bytes()); // log_blocksize
+    hash.update(&0u8.to_le_bytes()); // salt_size
+    hash.update(&0u32.to_le_bytes()); // sig_size
+    hash.update(&file_size.to_le_bytes()); // data_size
+    hash.update(root_hash); // root_hash, first 32 bytes
+    hash.update(&[0u8; 32]); // root_hash, last 32 bytes, always 0 because we are using sha256.
+    hash.update(&[0u8; 32]); // salt
+    hash.update(&[0u8; 32]); // reserved
+    hash.update(&[0u8; 32]); // reserved
+    hash.update(&[0u8; 32]); // reserved
+    hash.update(&[0u8; 32]); // reserved
+    hash.update(&[0u8; 16]); // reserved
+    hash.finish()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_merkle_tree_size() {
+        // To produce groundtruth:
+        //   dd if=/dev/zero of=zeros bs=1 count=524289 && \
+        //   fsverity digest --out-merkle-tree=tree zeros && \
+        //   du -b tree
+        assert_eq!(merkle_tree_size(0), 0);
+        assert_eq!(merkle_tree_size(1), 0);
+        assert_eq!(merkle_tree_size(4096), 0);
+        assert_eq!(merkle_tree_size(4097), 4096);
+        assert_eq!(merkle_tree_size(524288), 4096);
+        assert_eq!(merkle_tree_size(524289), 12288);
+    }
+}
diff --git a/guest/authfs/src/fsverity/editor.rs b/guest/authfs/src/fsverity/editor.rs
new file mode 100644
index 0000000..c84500b
--- /dev/null
+++ b/guest/authfs/src/fsverity/editor.rs
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! A module for writing to a file from a trusted world to an untrusted storage.
+//!
+//! Architectural Model:
+//!  * Trusted world: the writer, a signing secret, has some memory, but NO persistent storage.
+//!  * Untrusted world: persistent storage, assuming untrusted.
+//!  * IPC mechanism between trusted and untrusted world
+//!
+//! Use cases:
+//!  * In the trusted world, we want to generate a large file, sign it, and share the signature for
+//!    a third party to verify the file.
+//!  * In the trusted world, we want to read a previously signed file back with signature check
+//!    without having to touch the whole file.
+//!
+//! Requirements:
+//!  * Communication between trusted and untrusted world is not cheap, and files can be large.
+//!  * A file write pattern may not be sequential, neither does read.
+//!
+//! Considering the above, a technique similar to fs-verity is used. fs-verity uses an alternative
+//! hash function, a Merkle tree, to calculate the hash of file content. A file update at any
+//! location will propagate the hash update from the leaf to the root node. Unlike fs-verity, which
+//! assumes static files, to support write operation, we need to allow the file (thus tree) to
+//! update.
+//!
+//! For the trusted world to generate a large file with random write and hash it, the writer needs
+//! to hold some private information and update the Merkle tree during a file write (or even when
+//! the Merkle tree needs to be stashed to the untrusted storage).
+//!
+//! A write to a file must update the root hash. In order for the root hash to update, a tree
+//! walk to update from the write location to the root node is necessary. Importantly, in case when
+//! (part of) the Merkle tree needs to be read from the untrusted storage (e.g. not yet verified in
+//! cache), the original path must be verified by the trusted signature before the update to happen.
+//!
+//! Denial-of-service is a known weakness if the untrusted storage decides to simply remove the
+//! file. But there is nothing we can do in this architecture.
+//!
+//! Rollback attack is another possible attack, but can be addressed with a rollback counter when
+//! possible.
+
+use std::io;
+use std::sync::{Arc, RwLock};
+
+use super::builder::MerkleLeaves;
+use super::common::{Sha256Hash, SHA256_HASH_SIZE};
+use crate::common::{ChunkedSizeIter, CHUNK_SIZE};
+use crate::file::{ChunkBuffer, RandomWrite, ReadByChunk};
+use openssl::sha::{sha256, Sha256};
+
+fn debug_assert_usize_is_u64() {
+    // Since we don't need to support 32-bit CPU, make an assert to make conversion between
+    // u64 and usize easy below. Otherwise, we need to check `divide_roundup(offset + buf.len()
+    // <= usize::MAX` or handle `TryInto` errors.
+    debug_assert!(usize::MAX as u64 == u64::MAX, "Only 64-bit arch is supported");
+}
+
+/// VerifiedFileEditor provides an integrity layer to an underlying read-writable file, which may
+/// not be stored in a trusted environment. Only new, empty files are currently supported.
+pub struct VerifiedFileEditor<F: ReadByChunk + RandomWrite> {
+    file: F,
+    merkle_tree: Arc<RwLock<MerkleLeaves>>,
+}
+
+impl<F: ReadByChunk + RandomWrite> VerifiedFileEditor<F> {
+    /// Wraps a supposedly new file for integrity protection.
+    pub fn new(file: F) -> Self {
+        Self { file, merkle_tree: Arc::new(RwLock::new(MerkleLeaves::new())) }
+    }
+
+    /// Returns the fs-verity digest size in bytes.
+    pub fn get_fsverity_digest_size(&self) -> usize {
+        SHA256_HASH_SIZE
+    }
+
+    /// Calculates the fs-verity digest of the current file.
+    pub fn calculate_fsverity_digest(&self) -> io::Result<Sha256Hash> {
+        let merkle_tree = self.merkle_tree.read().unwrap();
+        merkle_tree.calculate_fsverity_digest().map_err(|e| io::Error::new(io::ErrorKind::Other, e))
+    }
+
+    fn read_backing_chunk_unverified(
+        &self,
+        chunk_index: u64,
+        buf: &mut ChunkBuffer,
+    ) -> io::Result<usize> {
+        self.file.read_chunk(chunk_index, buf)
+    }
+
+    fn read_backing_chunk_verified(
+        &self,
+        chunk_index: u64,
+        buf: &mut ChunkBuffer,
+        merkle_tree_locked: &MerkleLeaves,
+    ) -> io::Result<usize> {
+        debug_assert_usize_is_u64();
+
+        if merkle_tree_locked.is_index_valid(chunk_index as usize) {
+            let size = self.read_backing_chunk_unverified(chunk_index, buf)?;
+
+            // Ensure the returned buffer matches the known hash.
+            let hash = sha256(buf);
+            if !merkle_tree_locked.is_consistent(chunk_index as usize, &hash) {
+                return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
+            }
+            Ok(size)
+        } else {
+            Ok(0)
+        }
+    }
+
+    fn new_hash_for_incomplete_write(
+        &self,
+        source: &[u8],
+        offset_from_alignment: usize,
+        output_chunk_index: usize,
+        merkle_tree: &mut MerkleLeaves,
+    ) -> io::Result<Sha256Hash> {
+        // The buffer is initialized to 0 purposely. To calculate the block hash, the data is
+        // 0-padded to the block size. When a chunk read is less than a chunk, the initial value
+        // conveniently serves the padding purpose.
+        let mut orig_data = [0u8; CHUNK_SIZE as usize];
+
+        // If previous data exists, read back and verify against the known hash (since the
+        // storage / remote server is not trusted).
+        if merkle_tree.is_index_valid(output_chunk_index) {
+            self.read_backing_chunk_unverified(output_chunk_index as u64, &mut orig_data)?;
+
+            // Verify original content
+            let hash = sha256(&orig_data);
+            if !merkle_tree.is_consistent(output_chunk_index, &hash) {
+                return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
+            }
+        }
+
+        let mut ctx = Sha256::new();
+        ctx.update(&orig_data[..offset_from_alignment]);
+        ctx.update(source);
+        ctx.update(&orig_data[offset_from_alignment + source.len()..]);
+        Ok(ctx.finish())
+    }
+
+    fn new_chunk_hash(
+        &self,
+        source: &[u8],
+        offset_from_alignment: usize,
+        current_size: usize,
+        output_chunk_index: usize,
+        merkle_tree: &mut MerkleLeaves,
+    ) -> io::Result<Sha256Hash> {
+        if current_size as u64 == CHUNK_SIZE {
+            // Case 1: If the chunk is a complete one, just calculate the hash, regardless of
+            // write location.
+            Ok(sha256(source))
+        } else {
+            // Case 2: For an incomplete write, calculate the hash based on previous data (if
+            // any).
+            self.new_hash_for_incomplete_write(
+                source,
+                offset_from_alignment,
+                output_chunk_index,
+                merkle_tree,
+            )
+        }
+    }
+
+    pub fn size(&self) -> u64 {
+        self.merkle_tree.read().unwrap().file_size()
+    }
+}
+
+impl<F: ReadByChunk + RandomWrite> RandomWrite for VerifiedFileEditor<F> {
+    fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+        debug_assert_usize_is_u64();
+
+        // The write range may not be well-aligned with the chunk boundary. There are various cases
+        // to deal with:
+        //  1. A write of a full 4K chunk.
+        //  2. A write of an incomplete chunk, possibly beyond the original EOF.
+        //
+        // Note that a write beyond EOF can create a hole. But we don't need to handle it here
+        // because holes are zeros, and leaves in MerkleLeaves are hashes of 4096-zeros by
+        // default.
+
+        // Now iterate on the input data, considering the alignment at the destination.
+        for (output_offset, current_size) in
+            ChunkedSizeIter::new(buf.len(), offset, CHUNK_SIZE as usize)
+        {
+            // Lock the tree for the whole write for now. There may be room to improve to increase
+            // throughput.
+            let mut merkle_tree = self.merkle_tree.write().unwrap();
+
+            let offset_in_buf = (output_offset - offset) as usize;
+            let source = &buf[offset_in_buf..offset_in_buf + current_size];
+            let output_chunk_index = (output_offset / CHUNK_SIZE) as usize;
+            let offset_from_alignment = (output_offset % CHUNK_SIZE) as usize;
+
+            let new_hash = match self.new_chunk_hash(
+                source,
+                offset_from_alignment,
+                current_size,
+                output_chunk_index,
+                &mut merkle_tree,
+            ) {
+                Ok(hash) => hash,
+                Err(e) => {
+                    // Return early when any error happens before the right. Even if the hash is not
+                    // consistent for the current chunk, we can still consider the earlier writes
+                    // successful. Note that nothing persistent has been done in this iteration.
+                    let written = output_offset - offset;
+                    if written > 0 {
+                        return Ok(written as usize);
+                    }
+                    return Err(e);
+                }
+            };
+
+            // A failed, partial write here will make the backing file inconsistent to the (old)
+            // hash. Nothing can be done within this writer, but at least it still maintains the
+            // (original) integrity for the file. To matches what write(2) describes for an error
+            // case (though it's about direct I/O), "Partial data may be written ... should be
+            // considered inconsistent", an error below is propagated.
+            self.file.write_all_at(source, output_offset)?;
+
+            // Update the hash only after the write succeeds. Note that this only attempts to keep
+            // the tree consistent to what has been written regardless the actual state beyond the
+            // writer.
+            let size_at_least = offset.saturating_add(buf.len() as u64);
+            merkle_tree.update_hash(output_chunk_index, &new_hash, size_at_least);
+        }
+        Ok(buf.len())
+    }
+
+    fn resize(&self, size: u64) -> io::Result<()> {
+        debug_assert_usize_is_u64();
+
+        let mut merkle_tree = self.merkle_tree.write().unwrap();
+        // In case when we are truncating the file, we may need to recalculate the hash of the (new)
+        // last chunk. Since the content is provided by the untrusted backend, we need to read the
+        // data back first, verify it, then override the truncated portion with 0-padding for
+        // hashing. As an optimization, we only need to read the data back if the new size isn't a
+        // multiple of CHUNK_SIZE (since the hash is already correct).
+        //
+        // The same thing does not need to happen when the size is growing. Since the new extended
+        // data is always 0, we can just resize the `MerkleLeaves`, where a new hash is always
+        // calculated from 4096 zeros.
+        if size < merkle_tree.file_size() && size % CHUNK_SIZE > 0 {
+            let new_tail_size = (size % CHUNK_SIZE) as usize;
+            let chunk_index = size / CHUNK_SIZE;
+            if new_tail_size > 0 {
+                let mut buf: ChunkBuffer = [0; CHUNK_SIZE as usize];
+                let s = self.read_backing_chunk_verified(chunk_index, &mut buf, &merkle_tree)?;
+                debug_assert!(new_tail_size <= s);
+
+                let zeros = vec![0; CHUNK_SIZE as usize - new_tail_size];
+                let mut ctx = Sha256::new();
+                ctx.update(&buf[..new_tail_size]);
+                ctx.update(&zeros);
+                let new_hash = ctx.finish();
+                merkle_tree.update_hash(chunk_index as usize, &new_hash, size);
+            }
+        }
+
+        self.file.resize(size)?;
+        merkle_tree.resize(size as usize);
+
+        Ok(())
+    }
+}
+
+impl<F: ReadByChunk + RandomWrite> ReadByChunk for VerifiedFileEditor<F> {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        let merkle_tree = self.merkle_tree.read().unwrap();
+        self.read_backing_chunk_verified(chunk_index, buf, &merkle_tree)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    // Test data below can be generated by:
+    //  $ perl -e 'print "\x{00}" x 6000' > foo
+    //  $ perl -e 'print "\x{01}" x 5000' >> foo
+    //  $ fsverity digest foo
+    use super::*;
+    use anyhow::Result;
+    use std::cell::RefCell;
+    use std::convert::TryInto;
+
+    struct InMemoryEditor {
+        data: RefCell<Vec<u8>>,
+        fail_read: bool,
+    }
+
+    impl InMemoryEditor {
+        pub fn new() -> InMemoryEditor {
+            InMemoryEditor { data: RefCell::new(Vec::new()), fail_read: false }
+        }
+    }
+
+    impl RandomWrite for InMemoryEditor {
+        fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+            let begin: usize =
+                offset.try_into().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+            let end = begin + buf.len();
+            if end > self.data.borrow().len() {
+                self.data.borrow_mut().resize(end, 0);
+            }
+            self.data.borrow_mut().as_mut_slice()[begin..end].copy_from_slice(buf);
+            Ok(buf.len())
+        }
+
+        fn resize(&self, size: u64) -> io::Result<()> {
+            let size: usize =
+                size.try_into().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+            self.data.borrow_mut().resize(size, 0);
+            Ok(())
+        }
+    }
+
+    impl ReadByChunk for InMemoryEditor {
+        fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+            if self.fail_read {
+                return Err(io::Error::new(io::ErrorKind::Other, "test!"));
+            }
+
+            let borrowed = self.data.borrow();
+            let chunk = &borrowed
+                .chunks(CHUNK_SIZE as usize)
+                .nth(chunk_index as usize)
+                .ok_or_else(|| {
+                    io::Error::new(
+                        io::ErrorKind::InvalidInput,
+                        format!("read_chunk out of bound: index {}", chunk_index),
+                    )
+                })?;
+            buf[..chunk.len()].copy_from_slice(chunk);
+            Ok(chunk.len())
+        }
+    }
+
+    #[test]
+    fn test_writer() -> Result<()> {
+        let writer = InMemoryEditor::new();
+        let buf = [1; 4096];
+        assert_eq!(writer.data.borrow().len(), 0);
+
+        assert_eq!(writer.write_at(&buf, 16384)?, 4096);
+        assert_eq!(writer.data.borrow()[16384..16384 + 4096], buf);
+
+        assert_eq!(writer.write_at(&buf, 2048)?, 4096);
+        assert_eq!(writer.data.borrow()[2048..2048 + 4096], buf);
+
+        assert_eq!(writer.data.borrow().len(), 16384 + 4096);
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_no_write() -> Result<()> {
+        // Verify fs-verity hash without any write.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_from_zero() -> Result<()> {
+        // Verify a write of a full chunk.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")?
+                .as_slice()
+        );
+
+        // Verify a write of across multiple chunks.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 4097], 0)?, 4097);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("2901b849fda2d91e3929524561c4a47e77bb64734319759507b2029f18b9cc52")?
+                .as_slice()
+        );
+
+        // Verify another write of across multiple chunks.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 10000], 0)?, 10000);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("7545409b556071554d18973a29b96409588c7cda4edd00d5586b27a11e1a523b")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_unaligned() -> Result<()> {
+        // Verify small, unaligned write beyond EOF.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 5], 3)?, 5);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("a23fc5130d3d7b3323fc4b4a5e79d5d3e9ddf3a3f5872639e867713512c6702f")?
+                .as_slice()
+        );
+
+        // Verify bigger, unaligned write beyond EOF.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 6000], 4000)?, 6000);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("d16d4c1c186d757e646f76208b21254f50d7f07ea07b1505ff48b2a6f603f989")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_with_hole() -> Result<()> {
+        // Verify an aligned write beyond EOF with holes.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 4096], 4096)?, 4096);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("4df2aefd8c2a9101d1d8770dca3ede418232eabce766bb8e020395eae2e97103")?
+                .as_slice()
+        );
+
+        // Verify an unaligned write beyond EOF with holes.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 5000], 6000)?, 5000);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("47d5da26f6934484e260630a69eb2eebb21b48f69bc8fbf8486d1694b7dba94f")?
+                .as_slice()
+        );
+
+        // Just another example with a small write.
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 5], 16381)?, 5);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("8bd118821fb4aff26bb4b51d485cc481a093c68131b7f4f112e9546198449752")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_various_writes() -> Result<()> {
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
+        assert_eq!(file.write_at(&[1; 2048], 4096 + 2048)?, 2048);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("4c433d8640c888b629dc673d318cbb8d93b1eebcc784d9353e07f09f0dcfe707")?
+                .as_slice()
+        );
+        assert_eq!(file.write_at(&[1; 2048], 2048)?, 2048);
+        assert_eq!(file.write_at(&[1; 2048], 4096)?, 2048);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")?
+                .as_slice()
+        );
+        assert_eq!(file.write_at(&[0; 2048], 2048)?, 2048);
+        assert_eq!(file.write_at(&[0; 2048], 4096)?, 2048);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("4c433d8640c888b629dc673d318cbb8d93b1eebcc784d9353e07f09f0dcfe707")?
+                .as_slice()
+        );
+        assert_eq!(file.write_at(&[1; 4096], 2048)?, 4096);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")?
+                .as_slice()
+        );
+        assert_eq!(file.write_at(&[1; 2048], 8192)?, 2048);
+        assert_eq!(file.write_at(&[1; 2048], 8192 + 2048)?, 2048);
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("23cbac08371e6ee838ebcc7ae6512b939d2226e802337be7b383c3e046047d24")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_inconsistent_read() -> Result<()> {
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
+
+        // Replace the expected hash of the first/0-th chunk. An incomplete write will fail when it
+        // detects the inconsistent read.
+        {
+            let mut merkle_tree = file.merkle_tree.write().unwrap();
+            let overriding_hash = [42; SHA256_HASH_SIZE];
+            merkle_tree.update_hash(0, &overriding_hash, 8192);
+        }
+        assert!(file.write_at(&[1; 1], 2048).is_err());
+
+        // A write of full chunk can still succeed. Also fixed the inconsistency.
+        assert_eq!(file.write_at(&[1; 4096], 4096)?, 4096);
+
+        // Replace the expected hash of the second/1-th chunk. A write range from previous chunk can
+        // still succeed, but returns early due to an inconsistent read but still successfully. A
+        // resumed write will fail since no bytes can be written due to the same inconsistency.
+        {
+            let mut merkle_tree = file.merkle_tree.write().unwrap();
+            let overriding_hash = [42; SHA256_HASH_SIZE];
+            merkle_tree.update_hash(1, &overriding_hash, 8192);
+        }
+        assert_eq!(file.write_at(&[10; 8000], 0)?, 4096);
+        assert!(file.write_at(&[10; 8000 - 4096], 4096).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_verified_writer_failed_read_back() -> Result<()> {
+        let mut writer = InMemoryEditor::new();
+        writer.fail_read = true;
+        let file = VerifiedFileEditor::new(writer);
+        assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
+
+        // When a read back is needed, a read failure will fail to write.
+        assert!(file.write_at(&[1; 1], 2048).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_resize_to_same_size() -> Result<()> {
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
+
+        assert!(file.resize(2048).is_ok());
+        assert_eq!(file.size(), 2048);
+
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("fef1b4f19bb7a2cd944d7cdee44d1accb12726389ca5b0f61ac0f548ae40876f")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_resize_to_grow() -> Result<()> {
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
+
+        // Resize should grow with 0s.
+        assert!(file.resize(4096).is_ok());
+        assert_eq!(file.size(), 4096);
+
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("9e0e2745c21e4e74065240936d2047340d96a466680c3c9d177b82433e7a0bb1")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_resize_to_shrink() -> Result<()> {
+        let file = VerifiedFileEditor::new(InMemoryEditor::new());
+        assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
+
+        // Truncate.
+        file.resize(2048)?;
+        assert_eq!(file.size(), 2048);
+
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("fef1b4f19bb7a2cd944d7cdee44d1accb12726389ca5b0f61ac0f548ae40876f")?
+                .as_slice()
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn test_resize_to_shrink_with_read_failure() -> Result<()> {
+        let mut writer = InMemoryEditor::new();
+        writer.fail_read = true;
+        let file = VerifiedFileEditor::new(writer);
+        assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
+
+        // A truncate needs a read back. If the read fail, the resize should fail.
+        assert!(file.resize(2048).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn test_resize_to_shirink_to_chunk_boundary() -> Result<()> {
+        let mut writer = InMemoryEditor::new();
+        writer.fail_read = true;
+        let file = VerifiedFileEditor::new(writer);
+        assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
+
+        // Truncate to a chunk boundary. A read error doesn't matter since we won't need to
+        // recalcuate the leaf hash.
+        file.resize(4096)?;
+        assert_eq!(file.size(), 4096);
+
+        assert_eq!(
+            file.calculate_fsverity_digest()?,
+            hex::decode("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")?
+                .as_slice()
+        );
+        Ok(())
+    }
+}
diff --git a/guest/authfs/src/fsverity/metadata/Android.bp b/guest/authfs/src/fsverity/metadata/Android.bp
new file mode 100644
index 0000000..c874c2b
--- /dev/null
+++ b/guest/authfs/src/fsverity/metadata/Android.bp
@@ -0,0 +1,27 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_bindgen {
+    name: "libauthfs_fsverity_metadata_bindgen",
+    wrapper_src: "metadata.hpp",
+    defaults: ["avf_build_flags_rust"],
+    crate_name: "authfs_fsverity_metadata_bindgen",
+    source_stem: "metadata_bindings",
+    apex_available: ["com.android.virt"],
+}
+
+rust_library {
+    name: "libauthfs_fsverity_metadata",
+    crate_name: "authfs_fsverity_metadata",
+    defaults: ["avf_build_flags_rust"],
+    srcs: [
+        "metadata.rs",
+    ],
+    rustlibs: [
+        "libauthfs_fsverity_metadata_bindgen",
+        "libopenssl",
+    ],
+    edition: "2021",
+    apex_available: ["com.android.virt"],
+}
diff --git a/guest/authfs/src/fsverity/metadata/metadata.hpp b/guest/authfs/src/fsverity/metadata/metadata.hpp
new file mode 100644
index 0000000..f05740e
--- /dev/null
+++ b/guest/authfs/src/fsverity/metadata/metadata.hpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef AUTHFS_FSVERITY_METADATA_H
+#define AUTHFS_FSVERITY_METADATA_H
+
+// This file contains the format of fs-verity metadata (.fsv_meta).
+//
+// The header format of .fsv_meta is:
+//
+// +-----------+---------------------------------------------+------------+
+// |  Address  |  Description                                |    Size    |
+// +-----------+---------------------------------------------+------------+
+// |  0x0000   |  32-bit LE, version of the format           |     4      |
+// |           |                                             |            |
+// |  0x0004   |  fsverity_descriptor (see linux/fsverity.h) |    256     |
+// |           |                                             |            |
+// |  0x0104   |  32-bit LE, type of signature               |     4      |
+// |           |  (0: NONE, 1: PKCS7, 2: RAW)                |            |
+// |           |                                             |            |
+// |  0x0108   |  32-bit LE, size of signature               |     4      |
+// |           |                                             |            |
+// |  0x010C   |  signature                                  | See 0x0108 |
+// +-----------+---------------------------------------------+------------+
+//
+// After the header, merkle tree dump exists at the first 4K boundary. Usually it's 0x1000, but it
+// could be, for example, 0x2000 or 0x3000, depending on the size of header.
+//
+// TODO(b/193113326): sync with build/make/tools/releasetools/fsverity_metadata_generator.py
+
+#include <stddef.h>
+#include <stdint.h>
+#include <linux/fsverity.h>
+
+const uint64_t CHUNK_SIZE = 4096;
+
+// Give the macro value a name to export.
+const uint8_t FSVERITY_HASH_ALG_SHA256 = FS_VERITY_HASH_ALG_SHA256;
+
+enum class FSVERITY_SIGNATURE_TYPE : __le32 {
+    NONE = 0,
+    PKCS7 = 1,
+    RAW = 2,
+};
+
+struct fsverity_metadata_header {
+    __le32 version;
+    fsverity_descriptor descriptor;
+    FSVERITY_SIGNATURE_TYPE signature_type;
+    __le32 signature_size;
+} __attribute__((packed));
+
+#endif   // AUTHFS_FSVERITY_METADATA_H
diff --git a/guest/authfs/src/fsverity/metadata/metadata.rs b/guest/authfs/src/fsverity/metadata/metadata.rs
new file mode 100644
index 0000000..54d0145
--- /dev/null
+++ b/guest/authfs/src/fsverity/metadata/metadata.rs
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! Rust bindgen interface for FSVerity Metadata file (.fsv_meta)
+use authfs_fsverity_metadata_bindgen::{
+    fsverity_descriptor, fsverity_metadata_header, FSVERITY_HASH_ALG_SHA256,
+    FSVERITY_SIGNATURE_TYPE_NONE, FSVERITY_SIGNATURE_TYPE_PKCS7, FSVERITY_SIGNATURE_TYPE_RAW,
+};
+
+use openssl::sha::sha256;
+use std::cmp::min;
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::{self, Read, Seek};
+use std::mem::{size_of, zeroed};
+use std::os::unix::fs::{FileExt, MetadataExt};
+use std::path::{Path, PathBuf};
+use std::slice::from_raw_parts_mut;
+
+/// Offset of `descriptor` in `struct fsverity_metadatata_header`.
+const DESCRIPTOR_OFFSET: usize = 4;
+
+/// Structure for parsed metadata.
+pub struct FSVerityMetadata {
+    /// Header for the metadata.
+    pub header: fsverity_metadata_header,
+
+    /// fs-verity digest of the file, with hash algorithm defined in the fs-verity descriptor.
+    pub digest: Vec<u8>,
+
+    /// Optional signature for the metadata.
+    pub signature: Option<Vec<u8>>,
+
+    metadata_file: File,
+
+    merkle_tree_offset: u64,
+}
+
+impl FSVerityMetadata {
+    /// Read the raw Merkle tree from the metadata, if it exists. The API semantics is similar to a
+    /// regular pread(2), and may not return full requested buffer.
+    pub fn read_merkle_tree(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
+        let file_size = self.metadata_file.metadata()?.size();
+        let start = self.merkle_tree_offset + offset;
+        let end = min(file_size, start + buf.len() as u64);
+        let read_size = (end - start) as usize;
+        debug_assert!(read_size <= buf.len());
+        if read_size == 0 {
+            Ok(0)
+        } else {
+            self.metadata_file.read_exact_at(&mut buf[..read_size], start)?;
+            Ok(read_size)
+        }
+    }
+}
+
+/// Common block and page size in Linux.
+pub const CHUNK_SIZE: u64 = authfs_fsverity_metadata_bindgen::CHUNK_SIZE;
+
+/// Derive a path of metadata for a given path.
+/// e.g. "system/framework/foo.jar" -> "system/framework/foo.jar.fsv_meta"
+pub fn get_fsverity_metadata_path(path: &Path) -> PathBuf {
+    let mut os_string: OsString = path.into();
+    os_string.push(".fsv_meta");
+    os_string.into()
+}
+
+/// Parse metadata from given file, and returns a structure for the metadata.
+pub fn parse_fsverity_metadata(mut metadata_file: File) -> io::Result<Box<FSVerityMetadata>> {
+    let (header, digest) = {
+        // SAFETY: The header doesn't include any pointers.
+        let mut header: fsverity_metadata_header = unsafe { zeroed() };
+
+        // SAFETY: fsverity_metadata_header is packed, so reading/write from/to the back_buffer
+        // won't overflow.
+        let back_buffer = unsafe {
+            from_raw_parts_mut(
+                &mut header as *mut fsverity_metadata_header as *mut u8,
+                size_of::<fsverity_metadata_header>(),
+            )
+        };
+        metadata_file.read_exact(back_buffer)?;
+
+        // Digest needs to be calculated with the raw value (without changing the endianness).
+        let digest = match header.descriptor.hash_algorithm {
+            FSVERITY_HASH_ALG_SHA256 => Ok(sha256(
+                &back_buffer
+                    [DESCRIPTOR_OFFSET..DESCRIPTOR_OFFSET + size_of::<fsverity_descriptor>()],
+            )
+            .to_vec()),
+            alg => Err(io::Error::new(
+                io::ErrorKind::Other,
+                format!("Unsupported hash algorithm {}, continue (likely failing soon)", alg),
+            )),
+        }?;
+
+        // TODO(inseob): This doesn't seem ideal. Maybe we can consider nom?
+        header.version = u32::from_le(header.version);
+        header.descriptor.data_size = u64::from_le(header.descriptor.data_size);
+        header.signature_type = u32::from_le(header.signature_type);
+        header.signature_size = u32::from_le(header.signature_size);
+        (header, digest)
+    };
+
+    if header.version != 1 {
+        return Err(io::Error::new(io::ErrorKind::Other, "unsupported metadata version"));
+    }
+
+    let signature = match header.signature_type {
+        FSVERITY_SIGNATURE_TYPE_NONE => None,
+        FSVERITY_SIGNATURE_TYPE_PKCS7 | FSVERITY_SIGNATURE_TYPE_RAW => {
+            // TODO: unpad pkcs7?
+            let mut buf = vec![0u8; header.signature_size as usize];
+            metadata_file.read_exact(&mut buf)?;
+            Some(buf)
+        }
+        _ => return Err(io::Error::new(io::ErrorKind::Other, "unknown signature type")),
+    };
+
+    // merkle tree is at the next 4K boundary
+    let merkle_tree_offset =
+        (metadata_file.stream_position()? + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
+
+    Ok(Box::new(FSVerityMetadata { header, digest, signature, metadata_file, merkle_tree_offset }))
+}
diff --git a/guest/authfs/src/fsverity/sys.rs b/guest/authfs/src/fsverity/sys.rs
new file mode 100644
index 0000000..51e10a5
--- /dev/null
+++ b/guest/authfs/src/fsverity/sys.rs
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/// fs-verity version that we are using
+pub const FS_VERITY_VERSION: u8 = 1;
+
+/// Hash algorithm to use from linux/fsverity.h
+pub const FS_VERITY_HASH_ALG_SHA256: u8 = 1;
+
+/// Log 2 of the block size (only 4096 is supported now)
+pub const FS_VERITY_LOG_BLOCKSIZE: u8 = 12;
diff --git a/guest/authfs/src/fsverity/verifier.rs b/guest/authfs/src/fsverity/verifier.rs
new file mode 100644
index 0000000..1434b7e
--- /dev/null
+++ b/guest/authfs/src/fsverity/verifier.rs
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use libc::EIO;
+use std::io;
+
+use super::common::{build_fsverity_digest, merkle_tree_height, FsverityError, SHA256_HASH_SIZE};
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use crate::file::{ChunkBuffer, ReadByChunk};
+use openssl::sha::{sha256, Sha256};
+
+const ZEROS: [u8; CHUNK_SIZE as usize] = [0u8; CHUNK_SIZE as usize];
+
+type HashBuffer = [u8; SHA256_HASH_SIZE];
+
+fn hash_with_padding(chunk: &[u8], pad_to: usize) -> HashBuffer {
+    let padding_size = pad_to - chunk.len();
+    let mut ctx = Sha256::new();
+    ctx.update(chunk);
+    ctx.update(&ZEROS[..padding_size]);
+    ctx.finish()
+}
+
+fn verity_check<T: ReadByChunk>(
+    chunk: &[u8],
+    chunk_index: u64,
+    file_size: u64,
+    merkle_tree: &T,
+) -> Result<HashBuffer, FsverityError> {
+    // The caller should not be able to produce a chunk at the first place if `file_size` is 0. The
+    // current implementation expects to crash when a `ReadByChunk` implementation reads
+    // beyond the file size, including empty file.
+    assert_ne!(file_size, 0);
+
+    let chunk_hash = hash_with_padding(chunk, CHUNK_SIZE as usize);
+
+    // When the file is smaller or equal to CHUNK_SIZE, the root of Merkle tree is defined as the
+    // hash of the file content, plus padding.
+    if file_size <= CHUNK_SIZE {
+        return Ok(chunk_hash);
+    }
+
+    fsverity_walk(chunk_index, file_size, merkle_tree)?.try_fold(
+        chunk_hash,
+        |actual_hash, result| {
+            let (merkle_chunk, hash_offset_in_chunk) = result?;
+            let expected_hash =
+                &merkle_chunk[hash_offset_in_chunk..hash_offset_in_chunk + SHA256_HASH_SIZE];
+            if actual_hash != expected_hash {
+                return Err(FsverityError::CannotVerify);
+            }
+            Ok(hash_with_padding(&merkle_chunk, CHUNK_SIZE as usize))
+        },
+    )
+}
+
+/// Given a chunk index and the size of the file, returns an iterator that walks the Merkle tree
+/// from the leaf to the root. The iterator carries the slice of the chunk/node as well as the
+/// offset of the child node's hash. It is up to the iterator user to use the node and hash,
+/// e.g. for the actual verification.
+#[allow(clippy::needless_collect)]
+fn fsverity_walk<T: ReadByChunk>(
+    chunk_index: u64,
+    file_size: u64,
+    merkle_tree: &T,
+) -> Result<impl Iterator<Item = Result<([u8; 4096], usize), FsverityError>> + '_, FsverityError> {
+    let hashes_per_node = CHUNK_SIZE / SHA256_HASH_SIZE as u64;
+    debug_assert_eq!(hashes_per_node, 128u64);
+    let max_level = merkle_tree_height(file_size).expect("file should not be empty") as u32;
+    let root_to_leaf_steps = (0..=max_level)
+        .rev()
+        .map(|x| {
+            let leaves_per_hash = hashes_per_node.pow(x);
+            let leaves_size_per_hash = CHUNK_SIZE * leaves_per_hash;
+            let leaves_size_per_node = leaves_size_per_hash * hashes_per_node;
+            let nodes_at_level = divide_roundup(file_size, leaves_size_per_node);
+            let level_size = nodes_at_level * CHUNK_SIZE;
+            let offset_in_level = (chunk_index / leaves_per_hash) * SHA256_HASH_SIZE as u64;
+            (level_size, offset_in_level)
+        })
+        .scan(0, |level_offset, (level_size, offset_in_level)| {
+            let this_level_offset = *level_offset;
+            *level_offset += level_size;
+            let global_hash_offset = this_level_offset + offset_in_level;
+            Some(global_hash_offset)
+        })
+        .map(|global_hash_offset| {
+            let chunk_index = global_hash_offset / CHUNK_SIZE;
+            let hash_offset_in_chunk = (global_hash_offset % CHUNK_SIZE) as usize;
+            (chunk_index, hash_offset_in_chunk)
+        })
+        .collect::<Vec<_>>(); // Needs to collect first to be able to reverse below.
+
+    Ok(root_to_leaf_steps.into_iter().rev().map(move |(chunk_index, hash_offset_in_chunk)| {
+        let mut merkle_chunk = [0u8; 4096];
+        // read_chunk is supposed to return a full chunk, or an incomplete one at the end of the
+        // file. In the incomplete case, the hash is calculated with 0-padding to the chunk size.
+        // Therefore, we don't need to check the returned size here.
+        let _ = merkle_tree.read_chunk(chunk_index, &mut merkle_chunk)?;
+        Ok((merkle_chunk, hash_offset_in_chunk))
+    }))
+}
+
+pub struct VerifiedFileReader<F: ReadByChunk, M: ReadByChunk> {
+    pub file_size: u64,
+    chunked_file: F,
+    merkle_tree: M,
+    root_hash: HashBuffer,
+}
+
+impl<F: ReadByChunk, M: ReadByChunk> VerifiedFileReader<F, M> {
+    pub fn new(
+        chunked_file: F,
+        file_size: u64,
+        expected_digest: &[u8],
+        merkle_tree: M,
+    ) -> Result<VerifiedFileReader<F, M>, FsverityError> {
+        let mut buf = [0u8; CHUNK_SIZE as usize];
+        if file_size <= CHUNK_SIZE {
+            let _size = chunked_file.read_chunk(0, &mut buf)?;
+            // The rest of buffer is 0-padded.
+        } else {
+            let size = merkle_tree.read_chunk(0, &mut buf)?;
+            if buf.len() != size {
+                return Err(FsverityError::InsufficientData(size));
+            }
+        }
+        let root_hash = sha256(&buf[..]);
+        if expected_digest == build_fsverity_digest(&root_hash, file_size) {
+            // Once verified, use the root_hash for verification going forward.
+            Ok(VerifiedFileReader { chunked_file, file_size, merkle_tree, root_hash })
+        } else {
+            Err(FsverityError::InvalidDigest)
+        }
+    }
+}
+
+impl<F: ReadByChunk, M: ReadByChunk> ReadByChunk for VerifiedFileReader<F, M> {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        let size = self.chunked_file.read_chunk(chunk_index, buf)?;
+        let root_hash = verity_check(&buf[..size], chunk_index, self.file_size, &self.merkle_tree)
+            .map_err(|_| io::Error::from_raw_os_error(EIO))?;
+        if root_hash != self.root_hash {
+            Err(io::Error::from_raw_os_error(EIO))
+        } else {
+            Ok(size)
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::file::ReadByChunk;
+    use anyhow::Result;
+    use authfs_fsverity_metadata::{parse_fsverity_metadata, FSVerityMetadata};
+    use std::cmp::min;
+    use std::fs::File;
+    use std::os::unix::fs::FileExt;
+
+    struct LocalFileReader {
+        file: File,
+        size: u64,
+    }
+
+    impl LocalFileReader {
+        fn new(file: File) -> io::Result<LocalFileReader> {
+            let size = file.metadata()?.len();
+            Ok(LocalFileReader { file, size })
+        }
+
+        fn len(&self) -> u64 {
+            self.size
+        }
+    }
+
+    impl ReadByChunk for LocalFileReader {
+        fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+            let start = chunk_index * CHUNK_SIZE;
+            if start >= self.size {
+                return Ok(0);
+            }
+            let end = min(self.size, start + CHUNK_SIZE);
+            let read_size = (end - start) as usize;
+            debug_assert!(read_size <= buf.len());
+            self.file.read_exact_at(&mut buf[..read_size], start)?;
+            Ok(read_size)
+        }
+    }
+
+    type LocalVerifiedFileReader = VerifiedFileReader<LocalFileReader, MerkleTreeReader>;
+
+    pub struct MerkleTreeReader {
+        metadata: Box<FSVerityMetadata>,
+    }
+
+    impl ReadByChunk for MerkleTreeReader {
+        fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+            self.metadata.read_merkle_tree(chunk_index * CHUNK_SIZE, buf)
+        }
+    }
+
+    fn total_chunk_number(file_size: u64) -> u64 {
+        (file_size + 4095) / 4096
+    }
+
+    // Returns a reader with fs-verity verification and the file size.
+    fn new_reader_with_fsverity(
+        content_path: &str,
+        metadata_path: &str,
+    ) -> Result<(LocalVerifiedFileReader, u64)> {
+        let file_reader = LocalFileReader::new(File::open(content_path)?)?;
+        let file_size = file_reader.len();
+        let metadata = parse_fsverity_metadata(File::open(metadata_path)?)?;
+        Ok((
+            VerifiedFileReader::new(
+                file_reader,
+                file_size,
+                &metadata.digest.clone(),
+                MerkleTreeReader { metadata },
+            )?,
+            file_size,
+        ))
+    }
+
+    #[test]
+    fn fsverity_verify_full_read_4k() -> Result<()> {
+        let (file_reader, file_size) =
+            new_reader_with_fsverity("testdata/input.4k", "testdata/input.4k.fsv_meta")?;
+
+        for i in 0..total_chunk_number(file_size) {
+            let mut buf = [0u8; 4096];
+            assert!(file_reader.read_chunk(i, &mut buf).is_ok());
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn fsverity_verify_full_read_4k1() -> Result<()> {
+        let (file_reader, file_size) =
+            new_reader_with_fsverity("testdata/input.4k1", "testdata/input.4k1.fsv_meta")?;
+
+        for i in 0..total_chunk_number(file_size) {
+            let mut buf = [0u8; 4096];
+            assert!(file_reader.read_chunk(i, &mut buf).is_ok());
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn fsverity_verify_full_read_4m() -> Result<()> {
+        let (file_reader, file_size) =
+            new_reader_with_fsverity("testdata/input.4m", "testdata/input.4m.fsv_meta")?;
+
+        for i in 0..total_chunk_number(file_size) {
+            let mut buf = [0u8; 4096];
+            assert!(file_reader.read_chunk(i, &mut buf).is_ok());
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn fsverity_verify_bad_merkle_tree() -> Result<()> {
+        let (file_reader, _) = new_reader_with_fsverity(
+            "testdata/input.4m",
+            "testdata/input.4m.fsv_meta.bad_merkle", // First leaf node is corrupted.
+        )?;
+
+        // A lowest broken node (a 4K chunk that contains 128 sha256 hashes) will fail the read
+        // failure of the underlying chunks, but not before or after.
+        let mut buf = [0u8; 4096];
+        let num_hashes = 4096 / 32;
+        let last_index = num_hashes;
+        for i in 0..last_index {
+            assert!(file_reader.read_chunk(i, &mut buf).is_err());
+        }
+        assert!(file_reader.read_chunk(last_index, &mut buf).is_ok());
+        Ok(())
+    }
+}
diff --git a/guest/authfs/src/fusefs.rs b/guest/authfs/src/fusefs.rs
new file mode 100644
index 0000000..618b8ac
--- /dev/null
+++ b/guest/authfs/src/fusefs.rs
@@ -0,0 +1,1075 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+mod file;
+mod mount;
+
+use anyhow::{anyhow, bail, Result};
+use fuse::filesystem::{
+    Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, GetxattrReply,
+    SetattrValid, ZeroCopyReader, ZeroCopyWriter,
+};
+use fuse::sys::OpenOptions as FuseOpenOptions;
+use log::{error, trace, warn};
+use std::collections::{btree_map, BTreeMap};
+use std::convert::{TryFrom, TryInto};
+use std::ffi::{CStr, CString, OsStr};
+use std::io;
+use std::mem::{zeroed, MaybeUninit};
+use std::option::Option;
+use std::os::unix::ffi::OsStrExt;
+use std::path::{Component, Path, PathBuf};
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, RwLock};
+use std::time::Duration;
+
+use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
+use crate::file::{
+    validate_basename, Attr, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor,
+    RemoteFileEditor, RemoteFileReader,
+};
+use crate::fsstat::RemoteFsStatsReader;
+use crate::fsverity::VerifiedFileEditor;
+
+pub use self::file::LazyVerifiedReadonlyFile;
+pub use self::mount::mount_and_enter_message_loop;
+use self::mount::MAX_WRITE_BYTES;
+
+pub type Inode = u64;
+type Handle = u64;
+
+/// Maximum time for a file's metadata to be cached by the kernel. Since any file and directory
+/// changes (if not read-only) has to go through AuthFS to be trusted, the timeout can be maximum.
+const DEFAULT_METADATA_TIMEOUT: Duration = Duration::MAX;
+
+const ROOT_INODE: Inode = 1;
+
+/// `AuthFsEntry` defines the filesystem entry type supported by AuthFS.
+pub enum AuthFsEntry {
+    /// A read-only directory (writable during initialization). Root directory is an example.
+    ReadonlyDirectory { dir: InMemoryDir },
+    /// A file type that is verified against fs-verity signature (thus read-only). The file is
+    /// served from a remote server.
+    VerifiedReadonly { reader: LazyVerifiedReadonlyFile },
+    /// A file type that is a read-only passthrough from a file on a remote server.
+    UnverifiedReadonly { reader: RemoteFileReader, file_size: u64 },
+    /// A file type that is initially empty, and the content is stored on a remote server. File
+    /// integrity is guaranteed with private Merkle tree.
+    VerifiedNew { editor: VerifiedFileEditor<RemoteFileEditor>, attr: Attr },
+    /// A directory type that is initially empty. One can create new file (`VerifiedNew`) and new
+    /// directory (`VerifiedNewDirectory` itself) with integrity guaranteed within the VM.
+    VerifiedNewDirectory { dir: RemoteDirEditor, attr: Attr },
+}
+
+impl AuthFsEntry {
+    fn expect_empty_deletable_directory(&self) -> io::Result<()> {
+        match self {
+            AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                if dir.number_of_entries() == 0 {
+                    Ok(())
+                } else {
+                    Err(io::Error::from_raw_os_error(libc::ENOTEMPTY))
+                }
+            }
+            AuthFsEntry::ReadonlyDirectory { .. } => {
+                Err(io::Error::from_raw_os_error(libc::EACCES))
+            }
+            _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+        }
+    }
+}
+
+struct InodeState {
+    /// Actual inode entry.
+    entry: AuthFsEntry,
+
+    /// Number of `Handle`s (i.e. file descriptors) that are currently referring to the this inode.
+    ///
+    /// Technically, this does not matter to readonly entries, since they live forever. The
+    /// reference count is only needed for manageing lifetime of writable entries like
+    /// `VerifiedNew` and `VerifiedNewDirectory`. That is, when an entry is deleted, the actual
+    /// entry needs to stay alive until the reference count reaches zero.
+    ///
+    /// Note: This is not to be confused with hardlinks, which AuthFS doesn't currently implement.
+    handle_ref_count: AtomicU64,
+
+    /// Whether the inode is already unlinked, i.e. should be removed, once `handle_ref_count` is
+    /// down to zero.
+    unlinked: bool,
+}
+
+impl InodeState {
+    fn new(entry: AuthFsEntry) -> Self {
+        InodeState { entry, handle_ref_count: AtomicU64::new(0), unlinked: false }
+    }
+
+    fn new_with_ref_count(entry: AuthFsEntry, handle_ref_count: u64) -> Self {
+        InodeState { entry, handle_ref_count: AtomicU64::new(handle_ref_count), unlinked: false }
+    }
+}
+
+/// Data type that a directory implementation should be able to present its entry to `AuthFs`.
+#[derive(Clone)]
+pub struct AuthFsDirEntry {
+    pub inode: Inode,
+    pub name: CString,
+    pub is_dir: bool,
+}
+
+/// A snapshot of a directory entries for supporting `readdir` operation.
+///
+/// The `readdir` implementation is required by FUSE to not return any entries that have been
+/// returned previously (while it's fine to not return new entries). Snapshot is the easiest way to
+/// be compliant. See `fuse::filesystem::readdir` for more details.
+///
+/// A `DirEntriesSnapshot` is created on `opendir`, and is associated with the returned
+/// `Handle`/FD. The snapshot is deleted when the handle is released in `releasedir`.
+type DirEntriesSnapshot = Vec<AuthFsDirEntry>;
+
+/// An iterator for reading from `DirEntriesSnapshot`.
+pub struct DirEntriesSnapshotIterator {
+    /// A reference to the `DirEntriesSnapshot` in `AuthFs`.
+    snapshot: Arc<DirEntriesSnapshot>,
+
+    /// A value determined by `Self` to identify the last entry. 0 is a reserved value by FUSE to
+    /// mean reading from the beginning.
+    prev_offset: usize,
+}
+
+impl DirectoryIterator for DirEntriesSnapshotIterator {
+    fn next(&mut self) -> Option<DirEntry> {
+        // This iterator should not be the only reference to the snapshot. The snapshot should
+        // still be hold in `dir_handle_table`, i.e. when the FD is not yet closed.
+        //
+        // This code is unreachable when `readdir` is called with a closed FD. Only when the FD is
+        // not yet closed, `DirEntriesSnapshotIterator` can be created (but still short-lived
+        // during `readdir`).
+        debug_assert!(Arc::strong_count(&self.snapshot) >= 2);
+
+        // Since 0 is reserved, let's use 1-based index for the offset. This allows us to
+        // resume from the previous read in the snapshot easily.
+        let current_offset = if self.prev_offset == 0 {
+            1 // first element in the vector
+        } else {
+            self.prev_offset + 1 // next element in the vector
+        };
+        if current_offset > self.snapshot.len() {
+            None
+        } else {
+            let AuthFsDirEntry { inode, name, is_dir } = &self.snapshot[current_offset - 1];
+            let entry = DirEntry {
+                offset: current_offset as u64,
+                ino: *inode,
+                name,
+                type_: if *is_dir { libc::DT_DIR.into() } else { libc::DT_REG.into() },
+            };
+            self.prev_offset = current_offset;
+            Some(entry)
+        }
+    }
+}
+
+type DirHandleTable = BTreeMap<Handle, Arc<DirEntriesSnapshot>>;
+
+// AuthFS needs to be `Sync` to be used with the `fuse` crate.
+pub struct AuthFs {
+    /// Table for `Inode` to `InodeState` lookup.
+    inode_table: RwLock<BTreeMap<Inode, InodeState>>,
+
+    /// The next available inode number.
+    next_inode: AtomicU64,
+
+    /// Table for `Handle` to `Arc<DirEntriesSnapshot>` lookup. On `opendir`, a new directory
+    /// handle is created and the snapshot of the current directory is created. This is not
+    /// super efficient, but is the simplest way to be compliant to the FUSE contract (see
+    /// `fuse::filesystem::readdir`).
+    ///
+    /// Currently, no code locks `dir_handle_table` and `inode_table` at the same time to avoid
+    /// deadlock.
+    dir_handle_table: RwLock<DirHandleTable>,
+
+    /// The next available handle number.
+    next_handle: AtomicU64,
+
+    /// A reader to access the remote filesystem stats, which is supposed to be of "the" output
+    /// directory. We assume all output are stored in the same partition.
+    remote_fs_stats_reader: RemoteFsStatsReader,
+}
+
+// Implementation for preparing an `AuthFs` instance, before starting to serve.
+// TODO(victorhsieh): Consider implement a builder to separate the mutable initialization from the
+// immutable / interiorly mutable serving phase.
+impl AuthFs {
+    pub fn new(remote_fs_stats_reader: RemoteFsStatsReader) -> AuthFs {
+        let mut inode_table = BTreeMap::new();
+        inode_table.insert(
+            ROOT_INODE,
+            InodeState::new(AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() }),
+        );
+
+        AuthFs {
+            inode_table: RwLock::new(inode_table),
+            next_inode: AtomicU64::new(ROOT_INODE + 1),
+            dir_handle_table: RwLock::new(BTreeMap::new()),
+            next_handle: AtomicU64::new(1),
+            remote_fs_stats_reader,
+        }
+    }
+
+    /// Add an `AuthFsEntry` as `basename` to the filesystem root.
+    pub fn add_entry_at_root_dir(
+        &mut self,
+        basename: PathBuf,
+        entry: AuthFsEntry,
+    ) -> Result<Inode> {
+        validate_basename(&basename)?;
+        self.add_entry_at_ro_dir_by_path(ROOT_INODE, &basename, entry)
+    }
+
+    /// Add an `AuthFsEntry` by path from the `ReadonlyDirectory` represented by `dir_inode`. The
+    /// path must be a related path. If some ancestor directories do not exist, they will be
+    /// created (also as `ReadonlyDirectory`) automatically.
+    pub fn add_entry_at_ro_dir_by_path(
+        &mut self,
+        dir_inode: Inode,
+        path: &Path,
+        entry: AuthFsEntry,
+    ) -> Result<Inode> {
+        // 1. Make sure the parent directories all exist. Derive the entry's parent inode.
+        let parent_path =
+            path.parent().ok_or_else(|| anyhow!("No parent directory: {:?}", path))?;
+        let parent_inode =
+            parent_path.components().try_fold(dir_inode, |current_dir_inode, path_component| {
+                match path_component {
+                    Component::RootDir => bail!("Absolute path is not supported"),
+                    Component::Normal(name) => {
+                        let inode_table = self.inode_table.get_mut().unwrap();
+                        // Locate the internal directory structure.
+                        let current_dir_entry = &mut inode_table
+                            .get_mut(&current_dir_inode)
+                            .ok_or_else(|| {
+                                anyhow!("Unknown directory inode {}", current_dir_inode)
+                            })?
+                            .entry;
+                        let dir = match current_dir_entry {
+                            AuthFsEntry::ReadonlyDirectory { dir } => dir,
+                            _ => unreachable!("Not a ReadonlyDirectory"),
+                        };
+                        // Return directory inode. Create first if not exists.
+                        if let Some(existing_inode) = dir.lookup_inode(name.as_ref()) {
+                            Ok(existing_inode)
+                        } else {
+                            let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
+                            let new_dir_entry =
+                                AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() };
+
+                            // Actually update the tables.
+                            dir.add_dir(name.as_ref(), new_inode)?;
+                            if inode_table
+                                .insert(new_inode, InodeState::new(new_dir_entry))
+                                .is_some()
+                            {
+                                bail!("Unexpected to find a duplicated inode");
+                            }
+                            Ok(new_inode)
+                        }
+                    }
+                    _ => Err(anyhow!("Path is not canonical: {:?}", path)),
+                }
+            })?;
+
+        // 2. Insert the entry to the parent directory, as well as the inode table.
+        let inode_table = self.inode_table.get_mut().unwrap();
+        let inode_state = inode_table.get_mut(&parent_inode).expect("previously returned inode");
+        match &mut inode_state.entry {
+            AuthFsEntry::ReadonlyDirectory { dir } => {
+                let basename =
+                    path.file_name().ok_or_else(|| anyhow!("Bad file name: {:?}", path))?;
+                let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
+
+                // Actually update the tables.
+                dir.add_file(basename.as_ref(), new_inode)?;
+                if inode_table.insert(new_inode, InodeState::new(entry)).is_some() {
+                    bail!("Unexpected to find a duplicated inode");
+                }
+                Ok(new_inode)
+            }
+            _ => unreachable!("Not a ReadonlyDirectory"),
+        }
+    }
+}
+
+// Implementation for serving requests.
+impl AuthFs {
+    /// Handles the file associated with `inode` if found. This function returns whatever
+    /// `handle_fn` returns.
+    fn handle_inode<F, R>(&self, inode: &Inode, handle_fn: F) -> io::Result<R>
+    where
+        F: FnOnce(&AuthFsEntry) -> io::Result<R>,
+    {
+        let inode_table = self.inode_table.read().unwrap();
+        handle_inode_locked(&inode_table, inode, |inode_state| handle_fn(&inode_state.entry))
+    }
+
+    /// Adds a new entry `name` created by `create_fn` at `parent_inode`, with an initial ref count
+    /// of one.
+    ///
+    /// The operation involves two updates: adding the name with a new allocated inode to the
+    /// parent directory, and insert the new inode and the actual `AuthFsEntry` to the global inode
+    /// table.
+    ///
+    /// `create_fn` receives the parent directory, through which it can create the new entry at and
+    /// register the new inode to. Its returned entry is then added to the inode table.
+    fn create_new_entry_with_ref_count<F>(
+        &self,
+        parent_inode: Inode,
+        name: &CStr,
+        create_fn: F,
+    ) -> io::Result<Inode>
+    where
+        F: FnOnce(&mut AuthFsEntry, &Path, Inode) -> io::Result<AuthFsEntry>,
+    {
+        let mut inode_table = self.inode_table.write().unwrap();
+        let (new_inode, new_file_entry) = handle_inode_mut_locked(
+            &mut inode_table,
+            &parent_inode,
+            |InodeState { entry, .. }| {
+                let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
+                let basename: &Path = cstr_to_path(name);
+                let new_file_entry = create_fn(entry, basename, new_inode)?;
+                Ok((new_inode, new_file_entry))
+            },
+        )?;
+
+        if let btree_map::Entry::Vacant(entry) = inode_table.entry(new_inode) {
+            entry.insert(InodeState::new_with_ref_count(new_file_entry, 1));
+            Ok(new_inode)
+        } else {
+            unreachable!("Unexpected duplication of inode {}", new_inode);
+        }
+    }
+
+    fn open_dir_store_snapshot(
+        &self,
+        dir_entries: Vec<AuthFsDirEntry>,
+    ) -> io::Result<(Option<Handle>, FuseOpenOptions)> {
+        let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
+        let mut dir_handle_table = self.dir_handle_table.write().unwrap();
+        if let btree_map::Entry::Vacant(value) = dir_handle_table.entry(handle) {
+            value.insert(Arc::new(dir_entries));
+            Ok((Some(handle), FuseOpenOptions::empty()))
+        } else {
+            unreachable!("Unexpected to see new handle {} to existing in the table", handle);
+        }
+    }
+}
+
+fn check_access_mode(flags: u32, mode: libc::c_int) -> io::Result<()> {
+    if (flags & libc::O_ACCMODE as u32) == mode as u32 {
+        Ok(())
+    } else {
+        Err(io::Error::from_raw_os_error(libc::EACCES))
+    }
+}
+
+cfg_if::cfg_if! {
+    if #[cfg(all(any(target_arch = "aarch64", target_arch = "riscv64"),
+                 target_pointer_width = "64"))] {
+        fn blk_size() -> libc::c_int { CHUNK_SIZE as libc::c_int }
+    } else {
+        fn blk_size() -> libc::c_long { CHUNK_SIZE as libc::c_long }
+    }
+}
+
+#[allow(clippy::enum_variant_names)]
+enum AccessMode {
+    ReadOnly,
+    Variable(u32),
+}
+
+fn create_stat(
+    ino: libc::ino_t,
+    file_size: u64,
+    access_mode: AccessMode,
+) -> io::Result<libc::stat64> {
+    // SAFETY: stat64 is a plan C struct without pointer.
+    let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
+
+    st.st_ino = ino;
+    st.st_mode = match access_mode {
+        AccessMode::ReadOnly => {
+            // Until needed, let's just grant the owner access.
+            libc::S_IFREG | libc::S_IRUSR
+        }
+        AccessMode::Variable(mode) => libc::S_IFREG | mode,
+    };
+    st.st_nlink = 1;
+    st.st_uid = 0;
+    st.st_gid = 0;
+    st.st_size = libc::off64_t::try_from(file_size)
+        .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
+    st.st_blksize = blk_size();
+    // Per man stat(2), st_blocks is "Number of 512B blocks allocated".
+    st.st_blocks = libc::c_longlong::try_from(divide_roundup(file_size, 512))
+        .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
+    Ok(st)
+}
+
+fn create_dir_stat(
+    ino: libc::ino_t,
+    file_number: u16,
+    access_mode: AccessMode,
+) -> io::Result<libc::stat64> {
+    // SAFETY: stat64 is a plan C struct without pointer.
+    let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
+
+    st.st_ino = ino;
+    st.st_mode = match access_mode {
+        AccessMode::ReadOnly => {
+            // Until needed, let's just grant the owner access and search to group and others.
+            libc::S_IFDIR | libc::S_IXUSR | libc::S_IRUSR | libc::S_IXGRP | libc::S_IXOTH
+        }
+        AccessMode::Variable(mode) => libc::S_IFDIR | mode,
+    };
+
+    // 2 extra for . and ..
+    st.st_nlink = file_number
+        .checked_add(2)
+        .ok_or_else(|| io::Error::from_raw_os_error(libc::EOVERFLOW))?
+        .into();
+
+    st.st_uid = 0;
+    st.st_gid = 0;
+    Ok(st)
+}
+
+fn offset_to_chunk_index(offset: u64) -> u64 {
+    offset / CHUNK_SIZE
+}
+
+fn read_chunks<W: io::Write, T: ReadByChunk>(
+    mut w: W,
+    file: &T,
+    file_size: u64,
+    offset: u64,
+    size: u32,
+) -> io::Result<usize> {
+    let remaining = file_size.saturating_sub(offset);
+    let size_to_read = std::cmp::min(size as usize, remaining as usize);
+    let total = ChunkedSizeIter::new(size_to_read, offset, CHUNK_SIZE as usize).try_fold(
+        0,
+        |total, (current_offset, planned_data_size)| {
+            // TODO(victorhsieh): There might be a non-trivial way to avoid this copy. For example,
+            // instead of accepting a buffer, the writer could expose the final destination buffer
+            // for the reader to write to. It might not be generally applicable though, e.g. with
+            // virtio transport, the buffer may not be continuous.
+            let mut buf = [0u8; CHUNK_SIZE as usize];
+            let read_size = file.read_chunk(offset_to_chunk_index(current_offset), &mut buf)?;
+            if read_size < planned_data_size {
+                return Err(io::Error::from_raw_os_error(libc::ENODATA));
+            }
+
+            let begin = (current_offset % CHUNK_SIZE) as usize;
+            let end = begin + planned_data_size;
+            let s = w.write(&buf[begin..end])?;
+            if s != planned_data_size {
+                return Err(io::Error::from_raw_os_error(libc::EIO));
+            }
+            Ok(total + s)
+        },
+    )?;
+
+    Ok(total)
+}
+
+impl FileSystem for AuthFs {
+    type Inode = Inode;
+    type Handle = Handle;
+    type DirIter = DirEntriesSnapshotIterator;
+
+    fn max_buffer_size(&self) -> u32 {
+        MAX_WRITE_BYTES
+    }
+
+    fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
+        // Enable writeback cache for better performance especially since our bandwidth to the
+        // backend service is limited.
+        Ok(FsOptions::WRITEBACK_CACHE)
+    }
+
+    fn lookup(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result<Entry> {
+        let inode_table = self.inode_table.read().unwrap();
+
+        // Look up the entry's inode number in parent directory.
+        let inode =
+            handle_inode_locked(&inode_table, &parent, |inode_state| match &inode_state.entry {
+                AuthFsEntry::ReadonlyDirectory { dir } => {
+                    let path = cstr_to_path(name);
+                    dir.lookup_inode(path).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
+                }
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                    let path = cstr_to_path(name);
+                    dir.find_inode(path)
+                }
+                _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+            })?;
+
+        // Create the entry's stat if found.
+        let st = handle_inode_locked(
+            &inode_table,
+            &inode,
+            |InodeState { entry, handle_ref_count, .. }| {
+                let st = match entry {
+                    AuthFsEntry::ReadonlyDirectory { dir } => {
+                        create_dir_stat(inode, dir.number_of_entries(), AccessMode::ReadOnly)
+                    }
+                    AuthFsEntry::UnverifiedReadonly { file_size, .. } => {
+                        create_stat(inode, *file_size, AccessMode::ReadOnly)
+                    }
+                    AuthFsEntry::VerifiedReadonly { reader } => {
+                        create_stat(inode, reader.file_size()?, AccessMode::ReadOnly)
+                    }
+                    AuthFsEntry::VerifiedNew { editor, attr, .. } => {
+                        create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))
+                    }
+                    AuthFsEntry::VerifiedNewDirectory { dir, attr } => create_dir_stat(
+                        inode,
+                        dir.number_of_entries(),
+                        AccessMode::Variable(attr.mode()),
+                    ),
+                }?;
+                if handle_ref_count.fetch_add(1, Ordering::Relaxed) == u64::MAX {
+                    panic!("Handle reference count overflow");
+                }
+                Ok(st)
+            },
+        )?;
+
+        Ok(Entry {
+            inode,
+            generation: 0,
+            attr: st,
+            entry_timeout: DEFAULT_METADATA_TIMEOUT,
+            attr_timeout: DEFAULT_METADATA_TIMEOUT,
+        })
+    }
+
+    fn forget(&self, _ctx: Context, inode: Self::Inode, count: u64) {
+        let mut inode_table = self.inode_table.write().unwrap();
+        let delete_now = handle_inode_mut_locked(
+            &mut inode_table,
+            &inode,
+            |InodeState { handle_ref_count, unlinked, .. }| {
+                let current = handle_ref_count.get_mut();
+                if count > *current {
+                    error!(
+                        "Trying to decrease refcount of inode {} by {} (> current {})",
+                        inode, count, *current
+                    );
+                    panic!(); // log to logcat with error!
+                }
+                *current -= count;
+                Ok(*unlinked && *current == 0)
+            },
+        );
+
+        match delete_now {
+            Ok(true) => {
+                let _ignored = inode_table.remove(&inode).expect("Removed an existing entry");
+            }
+            Ok(false) => { /* Let the inode stay */ }
+            Err(e) => {
+                warn!(
+                    "Unexpected failure when tries to forget an inode {} by refcount {}: {:?}",
+                    inode, count, e
+                );
+            }
+        }
+    }
+
+    fn getattr(
+        &self,
+        _ctx: Context,
+        inode: Inode,
+        _handle: Option<Handle>,
+    ) -> io::Result<(libc::stat64, Duration)> {
+        self.handle_inode(&inode, |config| {
+            Ok((
+                match config {
+                    AuthFsEntry::ReadonlyDirectory { dir } => {
+                        create_dir_stat(inode, dir.number_of_entries(), AccessMode::ReadOnly)
+                    }
+                    AuthFsEntry::UnverifiedReadonly { file_size, .. } => {
+                        create_stat(inode, *file_size, AccessMode::ReadOnly)
+                    }
+                    AuthFsEntry::VerifiedReadonly { reader } => {
+                        create_stat(inode, reader.file_size()?, AccessMode::ReadOnly)
+                    }
+                    AuthFsEntry::VerifiedNew { editor, attr, .. } => {
+                        create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))
+                    }
+                    AuthFsEntry::VerifiedNewDirectory { dir, attr } => create_dir_stat(
+                        inode,
+                        dir.number_of_entries(),
+                        AccessMode::Variable(attr.mode()),
+                    ),
+                }?,
+                DEFAULT_METADATA_TIMEOUT,
+            ))
+        })
+    }
+
+    fn open(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        flags: u32,
+    ) -> io::Result<(Option<Self::Handle>, FuseOpenOptions)> {
+        // Since file handle is not really used in later operations (which use Inode directly),
+        // return None as the handle.
+        self.handle_inode(&inode, |config| {
+            match config {
+                AuthFsEntry::VerifiedReadonly { .. } | AuthFsEntry::UnverifiedReadonly { .. } => {
+                    check_access_mode(flags, libc::O_RDONLY)?;
+                }
+                AuthFsEntry::VerifiedNew { .. } => {
+                    // TODO(victorhsieh): Imeplement ACL check using the attr and ctx. Always allow
+                    // for now.
+                }
+                AuthFsEntry::ReadonlyDirectory { .. }
+                | AuthFsEntry::VerifiedNewDirectory { .. } => {
+                    // TODO(victorhsieh): implement when needed.
+                    return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+                }
+            }
+            // Always cache the file content. There is currently no need to support direct I/O or
+            // avoid the cache buffer. Memory mapping is only possible with cache enabled.
+            Ok((None, FuseOpenOptions::KEEP_CACHE))
+        })
+    }
+
+    fn create(
+        &self,
+        _ctx: Context,
+        parent: Self::Inode,
+        name: &CStr,
+        mode: u32,
+        _flags: u32,
+        umask: u32,
+        _security_ctx: Option<&CStr>,
+    ) -> io::Result<(Entry, Option<Self::Handle>, FuseOpenOptions)> {
+        let new_inode = self.create_new_entry_with_ref_count(
+            parent,
+            name,
+            |parent_entry, basename, new_inode| match parent_entry {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                    if dir.has_entry(basename) {
+                        return Err(io::Error::from_raw_os_error(libc::EEXIST));
+                    }
+                    let mode = mode & !umask;
+                    let (new_file, new_attr) = dir.create_file(basename, new_inode, mode)?;
+                    Ok(AuthFsEntry::VerifiedNew { editor: new_file, attr: new_attr })
+                }
+                _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+            },
+        )?;
+
+        Ok((
+            Entry {
+                inode: new_inode,
+                generation: 0,
+                attr: create_stat(new_inode, /* file_size */ 0, AccessMode::Variable(mode))?,
+                entry_timeout: DEFAULT_METADATA_TIMEOUT,
+                attr_timeout: DEFAULT_METADATA_TIMEOUT,
+            },
+            // See also `open`.
+            /* handle */ None,
+            FuseOpenOptions::KEEP_CACHE,
+        ))
+    }
+
+    fn read<W: io::Write + ZeroCopyWriter>(
+        &self,
+        _ctx: Context,
+        inode: Inode,
+        _handle: Handle,
+        w: W,
+        size: u32,
+        offset: u64,
+        _lock_owner: Option<u64>,
+        _flags: u32,
+    ) -> io::Result<usize> {
+        self.handle_inode(&inode, |config| {
+            match config {
+                AuthFsEntry::VerifiedReadonly { reader } => {
+                    read_chunks(w, reader, reader.file_size()?, offset, size)
+                }
+                AuthFsEntry::UnverifiedReadonly { reader, file_size } => {
+                    read_chunks(w, reader, *file_size, offset, size)
+                }
+                AuthFsEntry::VerifiedNew { editor, .. } => {
+                    // Note that with FsOptions::WRITEBACK_CACHE, it's possible for the kernel to
+                    // request a read even if the file is open with O_WRONLY.
+                    read_chunks(w, editor, editor.size(), offset, size)
+                }
+                AuthFsEntry::ReadonlyDirectory { .. }
+                | AuthFsEntry::VerifiedNewDirectory { .. } => {
+                    Err(io::Error::from_raw_os_error(libc::EISDIR))
+                }
+            }
+        })
+    }
+
+    fn write<R: io::Read + ZeroCopyReader>(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        _handle: Self::Handle,
+        mut r: R,
+        size: u32,
+        offset: u64,
+        _lock_owner: Option<u64>,
+        _delayed_write: bool,
+        _flags: u32,
+    ) -> io::Result<usize> {
+        self.handle_inode(&inode, |config| match config {
+            AuthFsEntry::VerifiedNew { editor, .. } => {
+                let mut buf = vec![0; size as usize];
+                r.read_exact(&mut buf)?;
+                editor.write_at(&buf, offset)
+            }
+            AuthFsEntry::VerifiedReadonly { .. } | AuthFsEntry::UnverifiedReadonly { .. } => {
+                Err(io::Error::from_raw_os_error(libc::EPERM))
+            }
+            AuthFsEntry::ReadonlyDirectory { .. } | AuthFsEntry::VerifiedNewDirectory { .. } => {
+                Err(io::Error::from_raw_os_error(libc::EISDIR))
+            }
+        })
+    }
+
+    fn setattr(
+        &self,
+        _ctx: Context,
+        inode: Inode,
+        in_attr: libc::stat64,
+        _handle: Option<Handle>,
+        valid: SetattrValid,
+    ) -> io::Result<(libc::stat64, Duration)> {
+        let mut inode_table = self.inode_table.write().unwrap();
+        handle_inode_mut_locked(&mut inode_table, &inode, |InodeState { entry, .. }| match entry {
+            AuthFsEntry::VerifiedNew { editor, attr } => {
+                check_unsupported_setattr_request(valid)?;
+
+                // Initialize the default stat.
+                let mut new_attr =
+                    create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))?;
+                // `valid` indicates what fields in `attr` are valid. Update to return correctly.
+                if valid.contains(SetattrValid::SIZE) {
+                    // st_size is i64, but the cast should be safe since kernel should not give a
+                    // negative size.
+                    debug_assert!(in_attr.st_size >= 0);
+                    new_attr.st_size = in_attr.st_size;
+                    editor.resize(in_attr.st_size as u64)?;
+                }
+                if valid.contains(SetattrValid::MODE) {
+                    attr.set_mode(in_attr.st_mode)?;
+                    new_attr.st_mode = in_attr.st_mode;
+                }
+                Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
+            }
+            AuthFsEntry::VerifiedNewDirectory { dir, attr } => {
+                check_unsupported_setattr_request(valid)?;
+                if valid.contains(SetattrValid::SIZE) {
+                    return Err(io::Error::from_raw_os_error(libc::EISDIR));
+                }
+
+                // Initialize the default stat.
+                let mut new_attr = create_dir_stat(
+                    inode,
+                    dir.number_of_entries(),
+                    AccessMode::Variable(attr.mode()),
+                )?;
+                if valid.contains(SetattrValid::MODE) {
+                    attr.set_mode(in_attr.st_mode)?;
+                    new_attr.st_mode = in_attr.st_mode;
+                }
+                Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
+            }
+            _ => Err(io::Error::from_raw_os_error(libc::EPERM)),
+        })
+    }
+
+    fn getxattr(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        name: &CStr,
+        size: u32,
+    ) -> io::Result<GetxattrReply> {
+        self.handle_inode(&inode, |config| {
+            match config {
+                AuthFsEntry::VerifiedNew { editor, .. } => {
+                    // FUSE ioctl is limited, thus we can't implement fs-verity ioctls without a
+                    // kernel change (see b/196635431). Until it's possible, use
+                    // xattr to expose what we need as an authfs specific API.
+                    if name != CStr::from_bytes_with_nul(b"authfs.fsverity.digest\0").unwrap() {
+                        return Err(io::Error::from_raw_os_error(libc::ENODATA));
+                    }
+
+                    if size == 0 {
+                        // Per protocol, when size is 0, return the value size.
+                        Ok(GetxattrReply::Count(editor.get_fsverity_digest_size() as u32))
+                    } else {
+                        let digest = editor.calculate_fsverity_digest()?;
+                        if digest.len() > size as usize {
+                            Err(io::Error::from_raw_os_error(libc::ERANGE))
+                        } else {
+                            Ok(GetxattrReply::Value(digest.to_vec()))
+                        }
+                    }
+                }
+                _ => Err(io::Error::from_raw_os_error(libc::ENODATA)),
+            }
+        })
+    }
+
+    fn mkdir(
+        &self,
+        _ctx: Context,
+        parent: Self::Inode,
+        name: &CStr,
+        mode: u32,
+        umask: u32,
+        _security_ctx: Option<&CStr>,
+    ) -> io::Result<Entry> {
+        let new_inode = self.create_new_entry_with_ref_count(
+            parent,
+            name,
+            |parent_entry, basename, new_inode| match parent_entry {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                    if dir.has_entry(basename) {
+                        return Err(io::Error::from_raw_os_error(libc::EEXIST));
+                    }
+                    let mode = mode & !umask;
+                    let (new_dir, new_attr) = dir.mkdir(basename, new_inode, mode)?;
+                    Ok(AuthFsEntry::VerifiedNewDirectory { dir: new_dir, attr: new_attr })
+                }
+                AuthFsEntry::ReadonlyDirectory { .. } => {
+                    Err(io::Error::from_raw_os_error(libc::EACCES))
+                }
+                _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+            },
+        )?;
+
+        Ok(Entry {
+            inode: new_inode,
+            generation: 0,
+            attr: create_dir_stat(new_inode, /* file_number */ 0, AccessMode::Variable(mode))?,
+            entry_timeout: DEFAULT_METADATA_TIMEOUT,
+            attr_timeout: DEFAULT_METADATA_TIMEOUT,
+        })
+    }
+
+    fn unlink(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<()> {
+        let mut inode_table = self.inode_table.write().unwrap();
+        handle_inode_mut_locked(
+            &mut inode_table,
+            &parent,
+            |InodeState { entry, unlinked, .. }| match entry {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                    let basename: &Path = cstr_to_path(name);
+                    // Delete the file from in both the local and remote directories.
+                    let _inode = dir.delete_file(basename)?;
+                    *unlinked = true;
+                    Ok(())
+                }
+                AuthFsEntry::ReadonlyDirectory { .. } => {
+                    Err(io::Error::from_raw_os_error(libc::EACCES))
+                }
+                AuthFsEntry::VerifiedNew { .. } => {
+                    // Deleting a entry in filesystem root is not currently supported.
+                    Err(io::Error::from_raw_os_error(libc::ENOSYS))
+                }
+                AuthFsEntry::UnverifiedReadonly { .. } | AuthFsEntry::VerifiedReadonly { .. } => {
+                    Err(io::Error::from_raw_os_error(libc::ENOTDIR))
+                }
+            },
+        )
+    }
+
+    fn rmdir(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<()> {
+        let mut inode_table = self.inode_table.write().unwrap();
+
+        // Check before actual removal, with readonly borrow.
+        handle_inode_locked(&inode_table, &parent, |inode_state| match &inode_state.entry {
+            AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                let basename: &Path = cstr_to_path(name);
+                let existing_inode = dir.find_inode(basename)?;
+                handle_inode_locked(&inode_table, &existing_inode, |inode_state| {
+                    inode_state.entry.expect_empty_deletable_directory()
+                })
+            }
+            AuthFsEntry::ReadonlyDirectory { .. } => {
+                Err(io::Error::from_raw_os_error(libc::EACCES))
+            }
+            _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+        })?;
+
+        // Look up again, this time with mutable borrow. This needs to be done separately because
+        // the previous lookup needs to borrow multiple entry references in the table.
+        handle_inode_mut_locked(
+            &mut inode_table,
+            &parent,
+            |InodeState { entry, unlinked, .. }| match entry {
+                AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+                    let basename: &Path = cstr_to_path(name);
+                    let _inode = dir.force_delete_directory(basename)?;
+                    *unlinked = true;
+                    Ok(())
+                }
+                _ => unreachable!("Mismatched entry type that is just checked"),
+            },
+        )
+    }
+
+    fn opendir(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        _flags: u32,
+    ) -> io::Result<(Option<Self::Handle>, FuseOpenOptions)> {
+        let entries = self.handle_inode(&inode, |config| match config {
+            AuthFsEntry::VerifiedNewDirectory { dir, .. } => dir.retrieve_entries(),
+            AuthFsEntry::ReadonlyDirectory { dir } => dir.retrieve_entries(),
+            _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+        })?;
+        self.open_dir_store_snapshot(entries)
+    }
+
+    fn readdir(
+        &self,
+        _ctx: Context,
+        _inode: Self::Inode,
+        handle: Self::Handle,
+        _size: u32,
+        offset: u64,
+    ) -> io::Result<Self::DirIter> {
+        let dir_handle_table = self.dir_handle_table.read().unwrap();
+        if let Some(entry) = dir_handle_table.get(&handle) {
+            Ok(DirEntriesSnapshotIterator {
+                snapshot: entry.clone(),
+                prev_offset: offset.try_into().unwrap(),
+            })
+        } else {
+            Err(io::Error::from_raw_os_error(libc::EBADF))
+        }
+    }
+
+    fn releasedir(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        _flags: u32,
+        handle: Self::Handle,
+    ) -> io::Result<()> {
+        let mut dir_handle_table = self.dir_handle_table.write().unwrap();
+        if dir_handle_table.remove(&handle).is_none() {
+            unreachable!("Unknown directory handle {}, inode {}", handle, inode);
+        }
+        Ok(())
+    }
+
+    fn statfs(&self, _ctx: Context, _inode: Self::Inode) -> io::Result<libc::statvfs64> {
+        let remote_stat = self.remote_fs_stats_reader.statfs()?;
+
+        // SAFETY: We are zero-initializing a struct with only POD fields. Not all fields matter to
+        // FUSE. See also:
+        // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/fuse/inode.c?h=v5.15#n460
+        let mut st: libc::statvfs64 = unsafe { zeroed() };
+
+        // Use the remote stat as a template, since it'd matter the most to consider the writable
+        // files/directories that are written to the remote.
+        st.f_bsize = remote_stat.block_size;
+        st.f_frsize = remote_stat.fragment_size;
+        st.f_blocks = remote_stat.block_numbers;
+        st.f_bavail = remote_stat.block_available;
+        st.f_favail = remote_stat.inodes_available;
+        st.f_namemax = remote_stat.max_filename;
+        // Assuming we are not privileged to use all free spaces on the remote server, set the free
+        // blocks/fragment to the same available amount.
+        st.f_bfree = st.f_bavail;
+        st.f_ffree = st.f_favail;
+        // Number of inodes on the filesystem
+        st.f_files = self.inode_table.read().unwrap().len() as u64;
+
+        Ok(st)
+    }
+}
+
+fn handle_inode_locked<F, R>(
+    inode_table: &BTreeMap<Inode, InodeState>,
+    inode: &Inode,
+    handle_fn: F,
+) -> io::Result<R>
+where
+    F: FnOnce(&InodeState) -> io::Result<R>,
+{
+    if let Some(inode_state) = inode_table.get(inode) {
+        handle_fn(inode_state)
+    } else {
+        Err(io::Error::from_raw_os_error(libc::ENOENT))
+    }
+}
+
+fn handle_inode_mut_locked<F, R>(
+    inode_table: &mut BTreeMap<Inode, InodeState>,
+    inode: &Inode,
+    handle_fn: F,
+) -> io::Result<R>
+where
+    F: FnOnce(&mut InodeState) -> io::Result<R>,
+{
+    if let Some(inode_state) = inode_table.get_mut(inode) {
+        handle_fn(inode_state)
+    } else {
+        Err(io::Error::from_raw_os_error(libc::ENOENT))
+    }
+}
+
+fn check_unsupported_setattr_request(valid: SetattrValid) -> io::Result<()> {
+    if valid.contains(SetattrValid::UID) {
+        warn!("Changing st_uid is not currently supported");
+        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+    }
+    if valid.contains(SetattrValid::GID) {
+        warn!("Changing st_gid is not currently supported");
+        return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+    }
+    if valid.intersects(
+        SetattrValid::CTIME
+            | SetattrValid::ATIME
+            | SetattrValid::ATIME_NOW
+            | SetattrValid::MTIME
+            | SetattrValid::MTIME_NOW,
+    ) {
+        trace!("Ignoring ctime/atime/mtime change as authfs does not maintain timestamp currently");
+    }
+    Ok(())
+}
+
+fn cstr_to_path(cstr: &CStr) -> &Path {
+    OsStr::from_bytes(cstr.to_bytes()).as_ref()
+}
diff --git a/guest/authfs/src/fusefs/file.rs b/guest/authfs/src/fusefs/file.rs
new file mode 100644
index 0000000..8c02281
--- /dev/null
+++ b/guest/authfs/src/fusefs/file.rs
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use log::error;
+use std::convert::TryInto;
+use std::io;
+use std::path::PathBuf;
+use std::sync::Mutex;
+
+use crate::file::{
+    ChunkBuffer, EagerChunkReader, ReadByChunk, RemoteFileReader, RemoteMerkleTreeReader,
+    VirtFdService,
+};
+use crate::fsverity::{merkle_tree_size, VerifiedFileReader};
+
+enum FileInfo {
+    ByPathUnderDirFd(i32, PathBuf),
+    ByFd(i32),
+}
+
+type Reader = VerifiedFileReader<RemoteFileReader, EagerChunkReader>;
+
+/// A lazily created read-only file that is verified against the given fs-verity digest.
+///
+/// The main purpose of this struct is to wrap and construct `VerifiedFileReader` lazily.
+pub struct LazyVerifiedReadonlyFile {
+    expected_digest: Vec<u8>,
+
+    service: VirtFdService,
+    file_info: FileInfo,
+
+    /// A lazily instantiated reader.
+    reader: Mutex<Option<Reader>>,
+}
+
+impl LazyVerifiedReadonlyFile {
+    /// Prepare the file by a remote path, related to a remote directory FD.
+    pub fn prepare_by_path(
+        service: VirtFdService,
+        remote_dir_fd: i32,
+        remote_path: PathBuf,
+        expected_digest: Vec<u8>,
+    ) -> Self {
+        LazyVerifiedReadonlyFile {
+            service,
+            file_info: FileInfo::ByPathUnderDirFd(remote_dir_fd, remote_path),
+            expected_digest,
+            reader: Mutex::new(None),
+        }
+    }
+
+    /// Prepare the file by a remote file FD.
+    pub fn prepare_by_fd(service: VirtFdService, remote_fd: i32, expected_digest: Vec<u8>) -> Self {
+        LazyVerifiedReadonlyFile {
+            service,
+            file_info: FileInfo::ByFd(remote_fd),
+            expected_digest,
+            reader: Mutex::new(None),
+        }
+    }
+
+    fn ensure_init_then<F, T>(&self, callback: F) -> io::Result<T>
+    where
+        F: FnOnce(&Reader) -> io::Result<T>,
+    {
+        let mut reader = self.reader.lock().unwrap();
+        if reader.is_none() {
+            let remote_file = match &self.file_info {
+                FileInfo::ByPathUnderDirFd(dir_fd, related_path) => {
+                    RemoteFileReader::new_by_path(self.service.clone(), *dir_fd, related_path)?
+                }
+                FileInfo::ByFd(file_fd) => RemoteFileReader::new(self.service.clone(), *file_fd),
+            };
+            let remote_fd = remote_file.get_remote_fd();
+            let file_size = self
+                .service
+                .getFileSize(remote_fd)
+                .map_err(|e| {
+                    error!("Failed to get file size of remote fd {}: {}", remote_fd, e);
+                    io::Error::from_raw_os_error(libc::EIO)
+                })?
+                .try_into()
+                .map_err(|e| {
+                    error!("Failed convert file size: {}", e);
+                    io::Error::from_raw_os_error(libc::EIO)
+                })?;
+            let instance = VerifiedFileReader::new(
+                remote_file,
+                file_size,
+                &self.expected_digest,
+                EagerChunkReader::new(
+                    RemoteMerkleTreeReader::new(self.service.clone(), remote_fd),
+                    merkle_tree_size(file_size),
+                )?,
+            )
+            .map_err(|e| {
+                error!("Failed instantiate a verified file reader: {}", e);
+                io::Error::from_raw_os_error(libc::EIO)
+            })?;
+            *reader = Some(instance);
+        }
+        callback(reader.as_ref().unwrap())
+    }
+
+    pub fn file_size(&self) -> io::Result<u64> {
+        self.ensure_init_then(|reader| Ok(reader.file_size))
+    }
+}
+
+impl ReadByChunk for LazyVerifiedReadonlyFile {
+    fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+        self.ensure_init_then(|reader| reader.read_chunk(chunk_index, buf))
+    }
+}
diff --git a/guest/authfs/src/fusefs/mount.rs b/guest/authfs/src/fusefs/mount.rs
new file mode 100644
index 0000000..7f8bac1
--- /dev/null
+++ b/guest/authfs/src/fusefs/mount.rs
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use fuse::mount::MountOption;
+use std::fs::OpenOptions;
+use std::num::NonZeroU8;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+
+use super::AuthFs;
+
+/// Maximum bytes (excluding the FUSE header) `AuthFs` will receive from the kernel for write
+/// operations by another process.
+pub const MAX_WRITE_BYTES: u32 = 65536;
+
+/// Maximum bytes (excluding the FUSE header) `AuthFs` will receive from the kernel for read
+/// operations by another process.
+/// TODO(victorhsieh): This option is deprecated by FUSE. Figure out if we can remove this.
+const MAX_READ_BYTES: u32 = 65536;
+
+/// Mount and start the FUSE instance to handle messages. This requires CAP_SYS_ADMIN.
+pub fn mount_and_enter_message_loop(
+    authfs: AuthFs,
+    mountpoint: &Path,
+    extra_options: &Option<String>,
+    threads: Option<NonZeroU8>,
+) -> Result<(), fuse::Error> {
+    let dev_fuse = OpenOptions::new()
+        .read(true)
+        .write(true)
+        .open("/dev/fuse")
+        .expect("Failed to open /dev/fuse");
+
+    let mut mount_options = vec![
+        MountOption::FD(dev_fuse.as_raw_fd()),
+        MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
+        MountOption::AllowOther,
+        MountOption::UserId(0),
+        MountOption::GroupId(0),
+        MountOption::MaxRead(MAX_READ_BYTES),
+    ];
+    if let Some(value) = extra_options {
+        mount_options.push(MountOption::Extra(value));
+    }
+
+    fuse::mount(
+        mountpoint,
+        "authfs",
+        libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
+        &mount_options,
+    )
+    .expect("Failed to mount fuse");
+
+    let mut config = fuse::FuseConfig::new();
+    config.dev_fuse(dev_fuse).max_write(MAX_WRITE_BYTES).max_read(MAX_READ_BYTES);
+    if let Some(num) = threads {
+        config.num_threads(u8::from(num).into());
+    }
+    config.enter_message_loop(authfs)
+}
diff --git a/guest/authfs/src/main.rs b/guest/authfs/src/main.rs
new file mode 100644
index 0000000..e46b197
--- /dev/null
+++ b/guest/authfs/src/main.rs
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! This crate implements AuthFS, a FUSE-based, non-generic filesystem where file access is
+//! authenticated. This filesystem assumes the underlying layer is not trusted, e.g. file may be
+//! provided by an untrusted host/VM, so that the content can't be simply trusted. However, with a
+//! known file hash from trusted party, this filesystem can still verify a (read-only) file even if
+//! the host/VM as the blob provider is malicious. With the Merkle tree, each read of file block can
+//! be verified individually only when needed.
+//!
+//! AuthFS only serve files that are specifically configured. Each remote file can be configured to
+//! appear as a local file at the mount point. A file configuration may include its remote file
+//! identifier and its verification method (e.g. by known digest).
+//!
+//! AuthFS also support remote directories. A remote directory may be defined by a manifest file,
+//! which contains file paths and their corresponding digests.
+//!
+//! AuthFS can also be configured for write, in which case the remote file server is treated as a
+//! (untrusted) storage. The file/directory integrity is maintained in memory in the VM. Currently,
+//! the state is not persistent, thus only new file/directory are supported.
+
+use anyhow::{anyhow, bail, Result};
+use clap::Parser;
+use log::error;
+use protobuf::Message;
+use std::convert::TryInto;
+use std::fs::File;
+use std::num::NonZeroU8;
+use std::path::{Path, PathBuf};
+
+mod common;
+mod file;
+mod fsstat;
+mod fsverity;
+mod fusefs;
+
+use file::{Attr, InMemoryDir, RemoteDirEditor, RemoteFileEditor, RemoteFileReader};
+use fsstat::RemoteFsStatsReader;
+use fsverity::VerifiedFileEditor;
+use fsverity_digests_proto::fsverity_digests::FSVerityDigests;
+use fusefs::{AuthFs, AuthFsEntry, LazyVerifiedReadonlyFile};
+
+#[derive(Parser)]
+struct Args {
+    /// Mount point of AuthFS.
+    mount_point: PathBuf,
+
+    /// CID of the VM where the service runs.
+    #[clap(long)]
+    cid: u32,
+
+    /// Extra options to FUSE
+    #[clap(short = 'o')]
+    extra_options: Option<String>,
+
+    /// Number of threads to serve FUSE requests.
+    #[clap(short = 'j')]
+    thread_number: Option<NonZeroU8>,
+
+    /// A read-only remote file with integrity check. Can be multiple.
+    ///
+    /// For example, `--remote-ro-file 5:sha256-1234abcd` tells the filesystem to associate the
+    /// file $MOUNTPOINT/5 with a remote FD 5, and has a fs-verity digest with sha256 of the hex
+    /// value 1234abcd.
+    #[clap(long, value_parser = parse_remote_ro_file_option)]
+    remote_ro_file: Vec<OptionRemoteRoFile>,
+
+    /// A read-only remote file without integrity check. Can be multiple.
+    ///
+    /// For example, `--remote-ro-file-unverified 5` tells the filesystem to associate the file
+    /// $MOUNTPOINT/5 with a remote FD 5.
+    #[clap(long)]
+    remote_ro_file_unverified: Vec<i32>,
+
+    /// A new read-writable remote file with integrity check. Can be multiple.
+    ///
+    /// For example, `--remote-new-rw-file 5` tells the filesystem to associate the file
+    /// $MOUNTPOINT/5 with a remote FD 5.
+    #[clap(long)]
+    remote_new_rw_file: Vec<i32>,
+
+    /// A read-only directory that represents a remote directory. The directory view is constructed
+    /// and finalized during the filesystem initialization based on the provided mapping file
+    /// (which is a serialized protobuf of android.security.fsverity.FSVerityDigests, which
+    /// essentially provides <file path, fs-verity digest> mappings of exported files). The mapping
+    /// file is supposed to come from a trusted location in order to provide a trusted view as well
+    /// as verified access of included files with their fs-verity digest. Not all files on the
+    /// remote host may be included in the mapping file, so the directory view may be partial. The
+    /// directory structure won't change throughout the filesystem lifetime.
+    ///
+    /// For example, `--remote-ro-dir 5:/path/to/mapping:prefix/` tells the filesystem to
+    /// construct a directory structure defined in the mapping file at $MOUNTPOINT/5, which may
+    /// include a file like /5/system/framework/framework.jar. "prefix/" tells the filesystem to
+    /// strip the path (e.g. "system/") from the mount point to match the expected location of the
+    /// remote FD (e.g. a directory FD of "/system" in the remote).
+    #[clap(long, value_parser = parse_remote_new_ro_dir_option)]
+    remote_ro_dir: Vec<OptionRemoteRoDir>,
+
+    /// A new directory that is assumed empty in the backing filesystem. New files created in this
+    /// directory are integrity-protected in the same way as --remote-new-verified-file. Can be
+    /// multiple.
+    ///
+    /// For example, `--remote-new-rw-dir 5` tells the filesystem to associate $MOUNTPOINT/5
+    /// with a remote dir FD 5.
+    #[clap(long)]
+    remote_new_rw_dir: Vec<i32>,
+
+    /// Enable debugging features.
+    #[clap(long)]
+    debug: bool,
+}
+
+#[derive(Clone)]
+struct OptionRemoteRoFile {
+    /// ID to refer to the remote file.
+    remote_fd: i32,
+
+    /// Expected fs-verity digest (with sha256) for the remote file.
+    digest: String,
+}
+
+#[derive(Clone)]
+struct OptionRemoteRoDir {
+    /// ID to refer to the remote dir.
+    remote_dir_fd: i32,
+
+    /// A mapping file that describes the expecting file/directory structure and integrity metadata
+    /// in the remote directory. The file contains serialized protobuf of
+    /// android.security.fsverity.FSVerityDigests.
+    mapping_file_path: PathBuf,
+
+    prefix: String,
+}
+
+fn parse_remote_ro_file_option(option: &str) -> Result<OptionRemoteRoFile> {
+    let strs: Vec<&str> = option.split(':').collect();
+    if strs.len() != 2 {
+        bail!("Invalid option: {}", option);
+    }
+    if let Some(digest) = strs[1].strip_prefix("sha256-") {
+        Ok(OptionRemoteRoFile { remote_fd: strs[0].parse::<i32>()?, digest: String::from(digest) })
+    } else {
+        bail!("Unsupported hash algorithm or invalid format: {}", strs[1]);
+    }
+}
+
+fn parse_remote_new_ro_dir_option(option: &str) -> Result<OptionRemoteRoDir> {
+    let strs: Vec<&str> = option.split(':').collect();
+    if strs.len() != 3 {
+        bail!("Invalid option: {}", option);
+    }
+    Ok(OptionRemoteRoDir {
+        remote_dir_fd: strs[0].parse::<i32>().unwrap(),
+        mapping_file_path: PathBuf::from(strs[1]),
+        prefix: String::from(strs[2]),
+    })
+}
+
+fn new_remote_verified_file_entry(
+    service: file::VirtFdService,
+    remote_fd: i32,
+    expected_digest: &str,
+) -> Result<AuthFsEntry> {
+    Ok(AuthFsEntry::VerifiedReadonly {
+        reader: LazyVerifiedReadonlyFile::prepare_by_fd(
+            service,
+            remote_fd,
+            hex::decode(expected_digest)?,
+        ),
+    })
+}
+
+fn new_remote_unverified_file_entry(
+    service: file::VirtFdService,
+    remote_fd: i32,
+    file_size: u64,
+) -> Result<AuthFsEntry> {
+    let reader = RemoteFileReader::new(service, remote_fd);
+    Ok(AuthFsEntry::UnverifiedReadonly { reader, file_size })
+}
+
+fn new_remote_new_verified_file_entry(
+    service: file::VirtFdService,
+    remote_fd: i32,
+) -> Result<AuthFsEntry> {
+    let remote_file = RemoteFileEditor::new(service.clone(), remote_fd);
+    Ok(AuthFsEntry::VerifiedNew {
+        editor: VerifiedFileEditor::new(remote_file),
+        attr: Attr::new_file(service, remote_fd),
+    })
+}
+
+fn new_remote_new_verified_dir_entry(
+    service: file::VirtFdService,
+    remote_fd: i32,
+) -> Result<AuthFsEntry> {
+    let dir = RemoteDirEditor::new(service.clone(), remote_fd);
+    let attr = Attr::new_dir(service, remote_fd);
+    Ok(AuthFsEntry::VerifiedNewDirectory { dir, attr })
+}
+
+fn prepare_root_dir_entries(
+    service: file::VirtFdService,
+    authfs: &mut AuthFs,
+    args: &Args,
+) -> Result<()> {
+    for config in &args.remote_ro_file {
+        authfs.add_entry_at_root_dir(
+            remote_fd_to_path_buf(config.remote_fd),
+            new_remote_verified_file_entry(service.clone(), config.remote_fd, &config.digest)?,
+        )?;
+    }
+
+    for remote_fd in &args.remote_ro_file_unverified {
+        let remote_fd = *remote_fd;
+        authfs.add_entry_at_root_dir(
+            remote_fd_to_path_buf(remote_fd),
+            new_remote_unverified_file_entry(
+                service.clone(),
+                remote_fd,
+                service.getFileSize(remote_fd)?.try_into()?,
+            )?,
+        )?;
+    }
+
+    for remote_fd in &args.remote_new_rw_file {
+        let remote_fd = *remote_fd;
+        authfs.add_entry_at_root_dir(
+            remote_fd_to_path_buf(remote_fd),
+            new_remote_new_verified_file_entry(service.clone(), remote_fd)?,
+        )?;
+    }
+
+    for remote_fd in &args.remote_new_rw_dir {
+        let remote_fd = *remote_fd;
+        authfs.add_entry_at_root_dir(
+            remote_fd_to_path_buf(remote_fd),
+            new_remote_new_verified_dir_entry(service.clone(), remote_fd)?,
+        )?;
+    }
+
+    for config in &args.remote_ro_dir {
+        let dir_root_inode = authfs.add_entry_at_root_dir(
+            remote_fd_to_path_buf(config.remote_dir_fd),
+            AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() },
+        )?;
+
+        // Build the directory tree based on the mapping file.
+        let mut reader = File::open(&config.mapping_file_path)?;
+        let proto = FSVerityDigests::parse_from_reader(&mut reader)?;
+        for (path_str, digest) in &proto.digests {
+            if digest.hash_alg != "sha256" {
+                bail!("Unsupported hash algorithm: {}", digest.hash_alg);
+            }
+
+            let file_entry = {
+                let remote_path_str = path_str.strip_prefix(&config.prefix).ok_or_else(|| {
+                    anyhow!("Expect path {} to match prefix {}", path_str, config.prefix)
+                })?;
+                AuthFsEntry::VerifiedReadonly {
+                    reader: LazyVerifiedReadonlyFile::prepare_by_path(
+                        service.clone(),
+                        config.remote_dir_fd,
+                        PathBuf::from(remote_path_str),
+                        digest.digest.clone(),
+                    ),
+                }
+            };
+            authfs.add_entry_at_ro_dir_by_path(dir_root_inode, Path::new(path_str), file_entry)?;
+        }
+    }
+
+    Ok(())
+}
+
+fn remote_fd_to_path_buf(fd: i32) -> PathBuf {
+    PathBuf::from(fd.to_string())
+}
+
+fn try_main() -> Result<()> {
+    let args = Args::parse();
+
+    let log_level = if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info };
+    android_logger::init_once(
+        android_logger::Config::default().with_tag("authfs").with_max_level(log_level),
+    );
+
+    let service = file::get_rpc_binder_service(args.cid)?;
+    let mut authfs = AuthFs::new(RemoteFsStatsReader::new(service.clone()));
+    prepare_root_dir_entries(service, &mut authfs, &args)?;
+
+    fusefs::mount_and_enter_message_loop(
+        authfs,
+        &args.mount_point,
+        &args.extra_options,
+        args.thread_number,
+    )?;
+    bail!("Unexpected exit after the handler loop")
+}
+
+fn main() {
+    if let Err(e) = try_main() {
+        error!("failed with {:?}", e);
+        std::process::exit(1);
+    }
+}