authfs: FUSE to serve file with fs-verity verification

The filesystem can currently serve local files specified via command
line flags, with verification using manually specified Merkle tree dump.
It also allows regular read without verification.

The change currently only supports local files for debug only. We will
need to add new configuration for remote file access with our own server
and protocol.

See tools/test.sh for the example setup.

BYPASS_INCLUSIVE_LANGUAGE_REASON=man page

Bug: 173507504
Test: atest --host authfs_host_test_src_lib
Test: tools/test.sh (on workstation)
Change-Id: I0ec14559fe8b4df2bd6fe5888018c12963958dc2
diff --git a/authfs/Android.bp b/authfs/Android.bp
index 3c5849b..fe85cff 100644
--- a/authfs/Android.bp
+++ b/authfs/Android.bp
@@ -2,18 +2,25 @@
     name: "authfs_defaults",
     crate_name: "authfs",
     srcs: [
-        "src/lib.rs",
+        "src/main.rs",
     ],
     edition: "2018",
     rustlibs: [
         "libanyhow",
         "libauthfs_crypto_bindgen",
+        "libcfg_if",
+        "libfuse_rust",
         "liblibc",
+        "libstructopt",
         "libthiserror",
     ],
     host_supported: true,
     shared_libs: ["libcrypto"],
     clippy_lints: "android",
+
+    // libfuse_rust currently has only the 64-bit variant (or more broadly speaking, the whole
+    // crosvm projects). Limit the build to 64 bits since we won't need 32 bits anyway.
+    compile_multilib: "64",
 }
 
 // TODO(b/172687320): remove once there is a canonical bindgen.
@@ -30,8 +37,8 @@
     host_supported: true,
 }
 
-rust_library {
-    name: "libauthfs",
+rust_binary {
+    name: "authfs",
     defaults: ["authfs_defaults"],
 }
 
diff --git a/authfs/src/common.rs b/authfs/src/common.rs
new file mode 100644
index 0000000..2220ae7
--- /dev/null
+++ b/authfs/src/common.rs
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+pub const COMMON_PAGE_SIZE: u64 = 4096;
+
+pub fn divide_roundup(dividend: u64, divisor: u64) -> u64 {
+    (dividend + divisor - 1) / divisor
+}
diff --git a/authfs/src/fsverity.rs b/authfs/src/fsverity.rs
index c9070ba..52aacf7 100644
--- a/authfs/src/fsverity.rs
+++ b/authfs/src/fsverity.rs
@@ -19,6 +19,7 @@
 use thiserror::Error;
 
 use crate::auth::Authenticator;
+use crate::common::divide_roundup;
 use crate::crypto::{CryptoError, Sha256Hasher};
 use crate::reader::ReadOnlyDataByChunk;
 
@@ -43,10 +44,6 @@
 
 type HashBuffer = [u8; Sha256Hasher::HASH_SIZE];
 
-fn divide_roundup(dividend: u64, divisor: u64) -> u64 {
-    (dividend + divisor - 1) / divisor
-}
-
 fn hash_with_padding(chunk: &[u8], pad_to: usize) -> Result<HashBuffer, CryptoError> {
     let padding_size = pad_to - chunk.len();
     Sha256Hasher::new()?.update(&chunk)?.update(&ZEROS[..padding_size])?.finalize()
@@ -168,7 +165,6 @@
 }
 
 impl<F: ReadOnlyDataByChunk, M: ReadOnlyDataByChunk> FsverityChunkedFileReader<F, M> {
-    #[allow(dead_code)]
     pub fn new<A: Authenticator>(
         authenticator: &A,
         chunked_file: F,
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
new file mode 100644
index 0000000..484aad4
--- /dev/null
+++ b/authfs/src/fusefs.rs
@@ -0,0 +1,338 @@
+/*
+ * 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 anyhow::Result;
+use std::collections::BTreeMap;
+use std::convert::TryFrom;
+use std::ffi::CStr;
+use std::fs::OpenOptions;
+use std::io;
+use std::mem::MaybeUninit;
+use std::option::Option;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+use std::time::Duration;
+
+use fuse::filesystem::{Context, DirEntry, DirectoryIterator, Entry, FileSystem, ZeroCopyWriter};
+use fuse::mount::MountOption;
+
+use crate::common::{divide_roundup, COMMON_PAGE_SIZE};
+use crate::fsverity::FsverityChunkedFileReader;
+use crate::reader::{ChunkedFileReader, ReadOnlyDataByChunk};
+
+// We're reading the backing file by chunk, so setting the block size to be the same.
+const BLOCK_SIZE: usize = COMMON_PAGE_SIZE as usize;
+
+const DEFAULT_METADATA_TIMEOUT: std::time::Duration = Duration::from_secs(5);
+
+pub type Inode = u64;
+type Handle = u64;
+
+// A debug only type where everything are stored as local files.
+type FileBackedFsverityChunkedFileReader =
+    FsverityChunkedFileReader<ChunkedFileReader, ChunkedFileReader>;
+
+pub enum FileConfig {
+    LocalVerifiedFile(FileBackedFsverityChunkedFileReader, u64),
+    LocalUnverifiedFile(ChunkedFileReader, u64),
+}
+
+struct AuthFs {
+    /// Store `FileConfig`s using the `Inode` number as the search index.
+    ///
+    /// For further optimization to minimize the search cost, since Inode is integer, we may
+    /// consider storing them in a Vec if we can guarantee that the numbers are small and
+    /// consecutive.
+    file_pool: BTreeMap<Inode, FileConfig>,
+
+    /// Maximum bytes in the write transaction to the FUSE device. This limits the maximum size to
+    /// a read request (including FUSE protocol overhead).
+    max_write: u32,
+}
+
+impl AuthFs {
+    pub fn new(file_pool: BTreeMap<Inode, FileConfig>, max_write: u32) -> AuthFs {
+        AuthFs { file_pool, max_write }
+    }
+
+    fn get_file_config(&self, inode: &Inode) -> io::Result<&FileConfig> {
+        self.file_pool.get(&inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
+    }
+}
+
+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(target_arch = "aarch64", target_pointer_width = "64"))] {
+        fn blk_size() -> libc::c_int { BLOCK_SIZE as libc::c_int }
+    } else {
+        fn blk_size() -> libc::c_long { BLOCK_SIZE as libc::c_long }
+    }
+}
+
+fn create_stat(ino: libc::ino_t, file_size: u64) -> io::Result<libc::stat64> {
+    let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
+
+    st.st_ino = ino;
+    st.st_mode = libc::S_IFREG | libc::S_IRUSR | libc::S_IRGRP | libc::S_IROTH;
+    st.st_dev = 0;
+    st.st_nlink = 1;
+    st.st_uid = 0;
+    st.st_gid = 0;
+    st.st_rdev = 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)
+}
+
+/// An iterator that generates (offset, size) for a chunked read operation, where offset is the
+/// global file offset, and size is the amount of read from the offset.
+struct ChunkReadIter {
+    remaining: usize,
+    offset: u64,
+}
+
+impl ChunkReadIter {
+    pub fn new(remaining: usize, offset: u64) -> Self {
+        ChunkReadIter { remaining, offset }
+    }
+}
+
+impl Iterator for ChunkReadIter {
+    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, BLOCK_SIZE - (self.offset % BLOCK_SIZE 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)
+    }
+}
+
+fn offset_to_chunk_index(offset: u64) -> u64 {
+    offset / BLOCK_SIZE as u64
+}
+
+fn read_chunks<W: io::Write, T: ReadOnlyDataByChunk>(
+    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 = ChunkReadIter::new(size_to_read, offset).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; BLOCK_SIZE];
+            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 % BLOCK_SIZE as u64) 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)
+}
+
+// No need to support enumerating directory entries.
+struct EmptyDirectoryIterator {}
+
+impl DirectoryIterator for EmptyDirectoryIterator {
+    fn next(&mut self) -> Option<DirEntry> {
+        None
+    }
+}
+
+impl FileSystem for AuthFs {
+    type Inode = Inode;
+    type Handle = Handle;
+    type DirIter = EmptyDirectoryIterator;
+
+    fn max_buffer_size(&self) -> u32 {
+        self.max_write
+    }
+
+    fn lookup(&self, _ctx: Context, _parent: Inode, name: &CStr) -> io::Result<Entry> {
+        // Only accept file name that looks like an integrer. Files in the pool are simply exposed
+        // by their inode number. Also, there is currently no directory structure.
+        let num = name.to_str().map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
+        // Normally, `lookup` is required to increase a reference count for the inode (while
+        // `forget` will decrease it). It is not necessary here since the files are configured to
+        // be static.
+        let inode = num.parse::<Inode>().map_err(|_| io::Error::from_raw_os_error(libc::ENOENT))?;
+        let st = match self.get_file_config(&inode)? {
+            FileConfig::LocalVerifiedFile(_, file_size)
+            | FileConfig::LocalUnverifiedFile(_, file_size) => create_stat(inode, *file_size)?,
+        };
+        Ok(Entry {
+            inode,
+            generation: 0,
+            attr: st,
+            entry_timeout: DEFAULT_METADATA_TIMEOUT,
+            attr_timeout: DEFAULT_METADATA_TIMEOUT,
+        })
+    }
+
+    fn getattr(
+        &self,
+        _ctx: Context,
+        inode: Inode,
+        _handle: Option<Handle>,
+    ) -> io::Result<(libc::stat64, Duration)> {
+        Ok((
+            match self.get_file_config(&inode)? {
+                FileConfig::LocalVerifiedFile(_, file_size)
+                | FileConfig::LocalUnverifiedFile(_, file_size) => create_stat(inode, *file_size)?,
+            },
+            DEFAULT_METADATA_TIMEOUT,
+        ))
+    }
+
+    fn open(
+        &self,
+        _ctx: Context,
+        inode: Self::Inode,
+        flags: u32,
+    ) -> io::Result<(Option<Self::Handle>, fuse::sys::OpenOptions)> {
+        // Since file handle is not really used in later operations (which use Inode directly),
+        // return None as the handle..
+        match self.get_file_config(&inode)? {
+            FileConfig::LocalVerifiedFile(_, _) => {
+                check_access_mode(flags, libc::O_RDONLY)?;
+                // Once verified, and only if verified, the file content can be cached. This is not
+                // really needed for a local file, but is the behavior of RemoteVerifiedFile later.
+                Ok((None, fuse::sys::OpenOptions::KEEP_CACHE))
+            }
+            FileConfig::LocalUnverifiedFile(_, _) => {
+                check_access_mode(flags, libc::O_RDONLY)?;
+                // Do not cache the content. This type of file is supposed to be verified using
+                // dm-verity. The filesystem mount over dm-verity already is already cached, so use
+                // direct I/O here to avoid double cache.
+                Ok((None, fuse::sys::OpenOptions::DIRECT_IO))
+            }
+        }
+    }
+
+    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> {
+        match self.get_file_config(&inode)? {
+            FileConfig::LocalVerifiedFile(file, file_size) => {
+                read_chunks(w, file, *file_size, offset, size)
+            }
+            FileConfig::LocalUnverifiedFile(file, file_size) => {
+                read_chunks(w, file, *file_size, offset, size)
+            }
+        }
+    }
+}
+
+/// Mount and start the FUSE instance. This requires CAP_SYS_ADMIN.
+pub fn loop_forever(
+    file_pool: BTreeMap<Inode, FileConfig>,
+    mountpoint: &Path,
+) -> Result<(), fuse::Error> {
+    let max_read: u32 = 65536;
+    let max_write: u32 = 65536;
+    let dev_fuse = OpenOptions::new()
+        .read(true)
+        .write(true)
+        .open("/dev/fuse")
+        .expect("Failed to open /dev/fuse");
+
+    fuse::mount(
+        mountpoint,
+        "authfs",
+        libc::MS_NOSUID | libc::MS_NODEV,
+        &[
+            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),
+        ],
+    )
+    .expect("Failed to mount fuse");
+
+    fuse::worker::start_message_loop(
+        dev_fuse,
+        max_write,
+        max_read,
+        AuthFs::new(file_pool, max_write),
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn collect_chunk_read_iter(remaining: usize, offset: u64) -> Vec<(u64, usize)> {
+        ChunkReadIter::new(remaining, offset).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/authfs/src/lib.rs b/authfs/src/lib.rs
deleted file mode 100644
index 05070d6..0000000
--- a/authfs/src/lib.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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 provides a FUSE-based, non-generic filesystem that I/O is authenticated. This
-//! filesystem assumes the storage layer is not trusted, e.g. file is provided by an untrusted VM,
-//! and the content can't be simply trusted. The filesystem can use its public key to verify a
-//! (read-only) file against its associated fs-verity signature by a trusted party. With the Merkle
-//! tree, each read of file block can be verified individually.
-//!
-//! The implementation is not finished.
-
-mod auth;
-mod crypto;
-mod fsverity;
-mod reader;
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
new file mode 100644
index 0000000..f0b5237
--- /dev/null
+++ b/authfs/src/main.rs
@@ -0,0 +1,155 @@
+/*
+ * 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
+//! public key from a trusted party, this filesystem can still verify a (read-only) file signed by
+//! the trusted party 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. A file configuration may include the
+//! source (e.g. local file or remote file server), verification method (e.g. certificate for
+//! fs-verity verification, or no verification if expected to mount over dm-verity), and file ID.
+//! Regardless of the actual file name, the exposed file names through AuthFS are currently integer,
+//! e.g. /mountpoint/42.
+
+use anyhow::{bail, Result};
+use std::collections::BTreeMap;
+use std::fs::File;
+use std::io::Read;
+use std::path::PathBuf;
+use structopt::StructOpt;
+
+mod auth;
+mod common;
+mod crypto;
+mod fsverity;
+mod fusefs;
+mod reader;
+
+use auth::FakeAuthenticator;
+use fsverity::FsverityChunkedFileReader;
+use fusefs::{FileConfig, Inode};
+use reader::ChunkedFileReader;
+
+#[derive(StructOpt)]
+struct Options {
+    /// Mount point of AuthFS.
+    #[structopt(parse(from_os_str))]
+    mount_point: PathBuf,
+
+    /// Debug only. A readonly file to be protected by fs-verity. Can be multiple.
+    #[structopt(long, parse(try_from_str = parse_local_verified_file_option))]
+    local_verified_file: Vec<LocalVerifiedFileConfig>,
+
+    /// Debug only. An unverified read-only file. Can be multiple.
+    #[structopt(long, parse(try_from_str = parse_local_unverified_file_option))]
+    local_unverified_file: Vec<LocalUnverifiedFileConfig>,
+}
+
+struct LocalVerifiedFileConfig {
+    ino: Inode,
+    file_path: PathBuf,
+    merkle_tree_dump_path: PathBuf,
+    signature_path: PathBuf,
+}
+
+struct LocalUnverifiedFileConfig {
+    ino: Inode,
+    file_path: PathBuf,
+}
+
+fn parse_local_verified_file_option(option: &str) -> Result<LocalVerifiedFileConfig> {
+    let strs: Vec<&str> = option.split(':').collect();
+    if strs.len() != 4 {
+        bail!("Invalid option: {}", option);
+    }
+    Ok(LocalVerifiedFileConfig {
+        ino: strs[0].parse::<Inode>().unwrap(),
+        file_path: PathBuf::from(strs[1]),
+        merkle_tree_dump_path: PathBuf::from(strs[2]),
+        signature_path: PathBuf::from(strs[3]),
+    })
+}
+
+fn parse_local_unverified_file_option(option: &str) -> Result<LocalUnverifiedFileConfig> {
+    let strs: Vec<&str> = option.split(':').collect();
+    if strs.len() != 2 {
+        bail!("Invalid option: {}", option);
+    }
+    Ok(LocalUnverifiedFileConfig {
+        ino: strs[0].parse::<Inode>().unwrap(),
+        file_path: PathBuf::from(strs[1]),
+    })
+}
+
+fn new_config_local_verified_file(
+    protected_file: &PathBuf,
+    merkle_tree_dump: &PathBuf,
+    signature: &PathBuf,
+) -> Result<FileConfig> {
+    let file = File::open(&protected_file)?;
+    let file_size = file.metadata()?.len();
+    let file_reader = ChunkedFileReader::new(file)?;
+    let merkle_tree_reader = ChunkedFileReader::new(File::open(merkle_tree_dump)?)?;
+    let authenticator = FakeAuthenticator::always_succeed();
+    let mut sig = Vec::new();
+    let _ = File::open(signature)?.read_to_end(&mut sig)?;
+    let file_reader = FsverityChunkedFileReader::new(
+        &authenticator,
+        file_reader,
+        file_size,
+        sig,
+        merkle_tree_reader,
+    )?;
+    Ok(FileConfig::LocalVerifiedFile(file_reader, file_size))
+}
+
+fn new_config_local_unverified_file(file_path: &PathBuf) -> Result<FileConfig> {
+    let file = File::open(file_path)?;
+    let file_size = file.metadata()?.len();
+    let file_reader = ChunkedFileReader::new(file)?;
+    Ok(FileConfig::LocalUnverifiedFile(file_reader, file_size))
+}
+
+fn prepare_file_pool(args: &Options) -> Result<BTreeMap<Inode, FileConfig>> {
+    let mut file_pool = BTreeMap::new();
+
+    for config in &args.local_verified_file {
+        file_pool.insert(
+            config.ino,
+            new_config_local_verified_file(
+                &config.file_path,
+                &config.merkle_tree_dump_path,
+                &config.signature_path,
+            )?,
+        );
+    }
+
+    for config in &args.local_unverified_file {
+        file_pool.insert(config.ino, new_config_local_unverified_file(&config.file_path)?);
+    }
+
+    Ok(file_pool)
+}
+
+fn main() -> Result<()> {
+    let args = Options::from_args();
+    let file_pool = prepare_file_pool(&args)?;
+    fusefs::loop_forever(file_pool, &args.mount_point)?;
+    Ok(())
+}
diff --git a/authfs/src/reader.rs b/authfs/src/reader.rs
index 135a793..2d1b617 100644
--- a/authfs/src/reader.rs
+++ b/authfs/src/reader.rs
@@ -19,13 +19,14 @@
 use std::fs::File;
 use std::io::Result;
 use std::os::unix::fs::FileExt;
-use std::path::Path;
+
+use crate::common::COMMON_PAGE_SIZE;
 
 /// A trait for reading data by chunks. The data is assumed readonly and has fixed length. Chunks
 /// can be read by specifying the chunk index. Only the last chunk may have incomplete chunk size.
 pub trait ReadOnlyDataByChunk {
     /// Default chunk size.
-    const CHUNK_SIZE: u64 = 4096;
+    const CHUNK_SIZE: u64 = COMMON_PAGE_SIZE;
 
     /// Read the `chunk_index`-th chunk to `buf`. Each slice/chunk has size `CHUNK_SIZE` except for
     /// the last one, which can be an incomplete chunk. `buf` is currently required to be large
@@ -49,9 +50,7 @@
 
 impl ChunkedFileReader {
     /// Creates a `ChunkedFileReader` to read from for the specified `path`.
-    #[allow(dead_code)]
-    pub fn new<P: AsRef<Path>>(path: P) -> Result<ChunkedFileReader> {
-        let file = File::open(path)?;
+    pub fn new(file: File) -> Result<ChunkedFileReader> {
         let size = file.metadata()?.len();
         Ok(ChunkedFileReader { file, size })
     }
diff --git a/authfs/tools/test.sh b/authfs/tools/test.sh
new file mode 100755
index 0000000..9ed3a99
--- /dev/null
+++ b/authfs/tools/test.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Run with -u to enter new namespace.
+if [[ $1 == "-u" ]]; then
+  exec unshare -m -U -r $0
+fi
+
+trap "umount /tmp/mnt" EXIT;
+mkdir -p /tmp/mnt
+
+echo "Mounting authfs in background ..."
+strace -o authfs.strace target/debug/authfs \
+  /tmp/mnt \
+  --local-verified-file 2:testdata/input.4m:testdata/input.4m.merkle_dump:testdata/input.4m.fsv_sig \
+  --local-verified-file 3:testdata/input.4k1:testdata/input.4k1.merkle_dump:testdata/input.4k1.fsv_sig \
+  --local-verified-file 4:testdata/input.4k:testdata/input.4k.merkle_dump:testdata/input.4k.fsv_sig \
+  --local-unverified-file 5:testdata/input.4k \
+  &
+sleep 0.1
+
+echo "Accessing files in authfs ..."
+echo
+md5sum /tmp/mnt/2 testdata/input.4m
+echo
+md5sum /tmp/mnt/3 testdata/input.4k1
+echo
+md5sum /tmp/mnt/4 /tmp/mnt/5 testdata/input.4k
+echo
+dd if=/tmp/mnt/2 bs=1000 skip=100 count=50 status=none |md5sum
+dd if=testdata/input.4m bs=1000 skip=100 count=50 status=none |md5sum
+echo
+tac /tmp/mnt/4 |md5sum
+tac /tmp/mnt/5 |md5sum
+tac testdata/input.4k |md5sum
+echo
+test -f /tmp/mnt/2 || echo 'FAIL: an expected file is missing'
+test -f /tmp/mnt/0 && echo 'FAIL: unexpected file presents'
+test -f /tmp/mnt/1 && echo 'FAIL: unexpected file presents, 1 is root dir'
+test -f /tmp/mnt/100 && echo 'FAIL: unexpected file presents'
+test -f /tmp/mnt/foo && echo 'FAIL: unexpected file presents'
+test -f /tmp/mnt/dir/3 && echo 'FAIL: unexpected file presents'
+echo "Done!"