authfs: Support write config/operation in fuse

Bug: 171279640
Test: atest
Test: tools/device-test.sh

Change-Id: Ic611f72d51a5522d9ec6e6fdc82c115b5782c4ac
diff --git a/authfs/src/file.rs b/authfs/src/file.rs
index 1d52631..89fbd9d 100644
--- a/authfs/src/file.rs
+++ b/authfs/src/file.rs
@@ -2,7 +2,7 @@
 mod remote_file;
 
 pub use local_file::LocalFileReader;
-pub use remote_file::{RemoteFileReader, RemoteMerkleTreeReader};
+pub use remote_file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
 
 use std::io;
 
diff --git a/authfs/src/file/remote_file.rs b/authfs/src/file/remote_file.rs
index b87891b..dbf6bd9 100644
--- a/authfs/src/file/remote_file.rs
+++ b/authfs/src/file/remote_file.rs
@@ -97,7 +97,6 @@
 }
 
 impl RemoteFileEditor {
-    #[allow(dead_code)]
     pub fn new(service: Arc<Mutex<VirtFdService>>, file_fd: i32) -> Self {
         RemoteFileEditor { service, file_fd }
     }
diff --git a/authfs/src/fsverity/builder.rs b/authfs/src/fsverity/builder.rs
index 86c4969..94b9718 100644
--- a/authfs/src/fsverity/builder.rs
+++ b/authfs/src/fsverity/builder.rs
@@ -53,6 +53,11 @@
         Self { leaves: Vec::new(), file_size: 0 }
     }
 
+    /// Gets size of the file represented by `MerkleLeaves`.
+    pub fn file_size(&self) -> u64 {
+        self.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) {
diff --git a/authfs/src/fsverity/editor.rs b/authfs/src/fsverity/editor.rs
index fc4e101..543e9ac 100644
--- a/authfs/src/fsverity/editor.rs
+++ b/authfs/src/fsverity/editor.rs
@@ -75,7 +75,6 @@
     merkle_tree: Arc<RwLock<MerkleLeaves>>,
 }
 
-#[allow(dead_code)]
 impl<F: ReadOnlyDataByChunk + RandomWrite> VerifiedFileEditor<F> {
     /// Wraps a supposedly new file for integrity protection.
     pub fn new(file: F) -> Self {
@@ -83,6 +82,7 @@
     }
 
     /// Calculates the fs-verity digest of the current file.
+    #[allow(dead_code)]
     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))
@@ -142,6 +142,10 @@
             )
         }
     }
+
+    pub fn size(&self) -> u64 {
+        self.merkle_tree.read().unwrap().file_size()
+    }
 }
 
 impl<F: ReadOnlyDataByChunk + RandomWrite> RandomWrite for VerifiedFileEditor<F> {
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
index bdf7cd8..13ec87d 100644
--- a/authfs/src/fusefs.rs
+++ b/authfs/src/fusefs.rs
@@ -26,12 +26,18 @@
 use std::path::Path;
 use std::time::Duration;
 
-use fuse::filesystem::{Context, DirEntry, DirectoryIterator, Entry, FileSystem, ZeroCopyWriter};
+use fuse::filesystem::{
+    Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, ZeroCopyReader,
+    ZeroCopyWriter,
+};
 use fuse::mount::MountOption;
 
 use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
-use crate::file::{LocalFileReader, ReadOnlyDataByChunk, RemoteFileReader, RemoteMerkleTreeReader};
-use crate::fsverity::VerifiedFileReader;
+use crate::file::{
+    LocalFileReader, RandomWrite, ReadOnlyDataByChunk, RemoteFileEditor, RemoteFileReader,
+    RemoteMerkleTreeReader,
+};
+use crate::fsverity::{VerifiedFileEditor, VerifiedFileReader};
 
 const DEFAULT_METADATA_TIMEOUT: std::time::Duration = Duration::from_secs(5);
 
@@ -43,6 +49,7 @@
     LocalUnverifiedReadonlyFile(LocalFileReader, u64),
     RemoteVerifiedReadonlyFile(VerifiedFileReader<RemoteFileReader, RemoteMerkleTreeReader>, u64),
     RemoteUnverifiedReadonlyFile(RemoteFileReader, u64),
+    RemoteVerifiedNewFile(VerifiedFileEditor<RemoteFileEditor>),
 }
 
 struct AuthFs {
@@ -84,11 +91,20 @@
     }
 }
 
-fn create_stat(ino: libc::ino_t, file_size: u64) -> io::Result<libc::stat64> {
+enum FileMode {
+    ReadOnly,
+    ReadWrite,
+}
+
+fn create_stat(ino: libc::ino_t, file_size: u64, file_mode: FileMode) -> 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_mode = match file_mode {
+        // Until needed, let's just grant the owner access.
+        FileMode::ReadOnly => libc::S_IFREG | libc::S_IRUSR,
+        FileMode::ReadWrite => libc::S_IFREG | libc::S_IRUSR | libc::S_IWUSR,
+    };
     st.st_dev = 0;
     st.st_nlink = 1;
     st.st_uid = 0;
@@ -160,6 +176,12 @@
         self.max_write
     }
 
+    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> {
         // 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.
@@ -173,7 +195,10 @@
             | FileConfig::LocalUnverifiedReadonlyFile(_, file_size)
             | FileConfig::RemoteUnverifiedReadonlyFile(_, file_size)
             | FileConfig::RemoteVerifiedReadonlyFile(_, file_size) => {
-                create_stat(inode, *file_size)?
+                create_stat(inode, *file_size, FileMode::ReadOnly)?
+            }
+            FileConfig::RemoteVerifiedNewFile(file) => {
+                create_stat(inode, file.size(), FileMode::ReadWrite)?
             }
         };
         Ok(Entry {
@@ -197,7 +222,10 @@
                 | FileConfig::LocalUnverifiedReadonlyFile(_, file_size)
                 | FileConfig::RemoteUnverifiedReadonlyFile(_, file_size)
                 | FileConfig::RemoteVerifiedReadonlyFile(_, file_size) => {
-                    create_stat(inode, *file_size)?
+                    create_stat(inode, *file_size, FileMode::ReadOnly)?
+                }
+                FileConfig::RemoteVerifiedNewFile(file) => {
+                    create_stat(inode, file.size(), FileMode::ReadWrite)?
                 }
             },
             DEFAULT_METADATA_TIMEOUT,
@@ -229,6 +257,11 @@
                 // direct I/O here to avoid double cache.
                 Ok((None, fuse::sys::OpenOptions::DIRECT_IO))
             }
+            FileConfig::RemoteVerifiedNewFile(_) => {
+                // No need to check access modes since all the modes are allowed to the
+                // read-writable file.
+                Ok((None, fuse::sys::OpenOptions::KEEP_CACHE))
+            }
         }
     }
 
@@ -256,6 +289,33 @@
             FileConfig::RemoteUnverifiedReadonlyFile(file, file_size) => {
                 read_chunks(w, file, *file_size, offset, size)
             }
+            FileConfig::RemoteVerifiedNewFile(file) => {
+                // 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, file, file.size(), offset, size)
+            }
+        }
+    }
+
+    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> {
+        match self.get_file_config(&inode)? {
+            FileConfig::RemoteVerifiedNewFile(file) => {
+                let mut buf = vec![0; size as usize];
+                r.read_exact(&mut buf)?;
+                file.write_at(&buf, offset)
+            }
+            _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
         }
     }
 }
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index 39482e3..a4b0d40 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -43,8 +43,8 @@
 mod fusefs;
 
 use auth::FakeAuthenticator;
-use file::{LocalFileReader, RemoteFileReader, RemoteMerkleTreeReader};
-use fsverity::VerifiedFileReader;
+use file::{LocalFileReader, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
+use fsverity::{VerifiedFileEditor, VerifiedFileReader};
 use fusefs::{FileConfig, Inode};
 
 #[derive(StructOpt)]
@@ -68,6 +68,13 @@
     #[structopt(long, parse(try_from_str = parse_remote_ro_file_unverified_option))]
     remote_ro_file_unverified: Vec<OptionRemoteRoFileUnverified>,
 
+    /// A new read-writable remote file with integrity check. Can be multiple.
+    ///
+    /// For example, `--remote-new-verified-file 12:34` tells the filesystem to associate entry 12
+    /// with a remote file 34.
+    #[structopt(long, parse(try_from_str = parse_remote_new_rw_file_option))]
+    remote_new_rw_file: Vec<OptionRemoteRwFile>,
+
     /// Debug only. A read-only local file with integrity check. Can be multiple.
     #[structopt(long, parse(try_from_str = parse_local_file_ro_option))]
     local_ro_file: Vec<OptionLocalFileRo>,
@@ -102,6 +109,13 @@
     file_size: u64,
 }
 
+struct OptionRemoteRwFile {
+    ino: Inode,
+
+    /// ID to refer to the remote file.
+    remote_id: i32,
+}
+
 struct OptionLocalFileRo {
     ino: Inode,
 
@@ -151,6 +165,17 @@
     })
 }
 
+fn parse_remote_new_rw_file_option(option: &str) -> Result<OptionRemoteRwFile> {
+    let strs: Vec<&str> = option.split(':').collect();
+    if strs.len() != 2 {
+        bail!("Invalid option: {}", option);
+    }
+    Ok(OptionRemoteRwFile {
+        ino: strs[0].parse::<Inode>().unwrap(),
+        remote_id: strs[1].parse::<i32>().unwrap(),
+    })
+}
+
 fn parse_local_file_ro_option(option: &str) -> Result<OptionLocalFileRo> {
     let strs: Vec<&str> = option.split(':').collect();
     if strs.len() != 5 {
@@ -223,6 +248,12 @@
     Ok(FileConfig::LocalUnverifiedReadonlyFile(file_reader, file_size))
 }
 
+fn new_config_remote_new_verified_file(remote_id: i32) -> Result<FileConfig> {
+    let remote_file =
+        RemoteFileEditor::new(Arc::new(Mutex::new(file::get_local_binder())), remote_id);
+    Ok(FileConfig::RemoteVerifiedNewFile(VerifiedFileEditor::new(remote_file)))
+}
+
 fn prepare_file_pool(args: &Args) -> Result<BTreeMap<Inode, FileConfig>> {
     let mut file_pool = BTreeMap::new();
 
@@ -240,6 +271,10 @@
         );
     }
 
+    for config in &args.remote_new_rw_file {
+        file_pool.insert(config.ino, new_config_remote_new_verified_file(config.remote_id)?);
+    }
+
     for config in &args.local_ro_file {
         file_pool.insert(
             config.ino,
diff --git a/authfs/tools/device-test.sh b/authfs/tools/device-test.sh
index e85ad17..82aa6bc 100755
--- a/authfs/tools/device-test.sh
+++ b/authfs/tools/device-test.sh
@@ -6,20 +6,28 @@
 #
 # Setup:
 # $ adb push testdata/input.4m* /data/local/tmp
+# $ adb push tools/device-test.sh /data/local/tmp/
 #
 # Shell 1:
-# $ adb shell 'cd /data/local/tmp && exec 9</system/bin/sh 8<input.4m 7<input.4m.merkle_dump 6<input.4m.fsv_sig 5<input.4m 4<input.4m.merkle_dump.bad 3<input.4m.fsv_sig fd_server --ro-fds 9 --ro-fds 8:7:6 --ro-fds 5:4:3'
+# $ adb shell /data/local/tmp/device-test.sh --run-fd-server
 #
 # Shell 2:
-# $ adb push tools/device-test.sh /data/local/tmp/ && adb shell /data/local/tmp/device-test.sh
+# $ adb shell /data/local/tmp/device-test.sh
+
+cd /data/local/tmp
+cat /dev/null > output
+
+if [[ $1 == "--run-fd-server" ]]; then
+  exec 9</system/bin/sh 8<input.4m 7<input.4m.merkle_dump 6<input.4m \
+    5<input.4m.merkle_dump.bad 4<input.4m.fsv_sig 3<>output \
+    fd_server --ro-fds 9 --ro-fds 8:7:4 --ro-fds 6:5:4 --rw-fds 3
+fi
 
 # Run with -u to enter new namespace.
 if [[ $1 == "-u" ]]; then
   exec unshare -mUr $0
 fi
 
-cd /data/local/tmp
-
 MOUNTPOINT=/data/local/tmp/authfs
 trap "umount ${MOUNTPOINT}" EXIT;
 mkdir -p ${MOUNTPOINT}
@@ -39,7 +47,8 @@
   --local-ro-file-unverified 5:/system/bin/sh \
   --remote-ro-file-unverified 6:9:${size} \
   --remote-ro-file 7:8:${size2}:/dev/null \
-  --remote-ro-file 8:5:${size2}:/dev/null \
+  --remote-ro-file 8:6:${size2}:/dev/null \
+  --remote-new-rw-file 9:3 \
   &
 sleep 0.1
 
@@ -55,6 +64,9 @@
 echo
 md5sum ${MOUNTPOINT}/7 input.4m
 echo
+cat input.4m > ${MOUNTPOINT}/9
+md5sum ${MOUNTPOINT}/9 output
+echo
 echo Checking error cases...
 cat /data/local/tmp/authfs/8 2>&1 |grep -q ": I/O error" || echo "Failed to catch the problem"
 echo "Done!"