Merge changes I89c9c96d,I9c7b68d5,I6b3f2b0c

* changes:
  Fix test action that tampered file at wrong offset
  Verify the last incomplete chunk on resize
  Verify read_chunk for writable file
diff --git a/authfs/src/fsverity/editor.rs b/authfs/src/fsverity/editor.rs
index f1e7529..60b5b80 100644
--- a/authfs/src/fsverity/editor.rs
+++ b/authfs/src/fsverity/editor.rs
@@ -52,6 +52,7 @@
 //! Rollback attack is another possible attack, but can be addressed with a rollback counter when
 //! possible.
 
+use log::warn;
 use std::io;
 use std::sync::{Arc, RwLock};
 
@@ -99,6 +100,44 @@
         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> {
+        let size = self.read_backing_chunk_unverified(chunk_index, buf)?;
+
+        // Ensure the returned buffer matches the known hash.
+        debug_assert_usize_is_u64();
+        if merkle_tree_locked.is_index_valid(chunk_index as usize) {
+            let hash = Sha256Hasher::new()?.update(buf)?.finalize()?;
+            if !merkle_tree_locked.is_consistent(chunk_index as usize, &hash) {
+                return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
+            }
+            Ok(size)
+        } else {
+            if size != 0 {
+                // This is unexpected. For any reason that the file is changed and doesn't match
+                // the known state, ignore it at the moment. We can still generate correct
+                // fs-verity digest for an output file.
+                warn!(
+                    "Ignoring the received {} bytes for index {} beyond the known file size",
+                    size, chunk_index,
+                );
+            }
+            Ok(0)
+        }
+    }
+
     fn new_hash_for_incomplete_write(
         &self,
         source: &[u8],
@@ -114,7 +153,7 @@
         // 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_chunk(output_chunk_index as u64, &mut orig_data)?;
+            self.read_backing_chunk_unverified(output_chunk_index as u64, &mut orig_data)?;
 
             // Verify original content
             let hash = Sha256Hasher::new()?.update(&orig_data)?.finalize()?;
@@ -239,7 +278,7 @@
             let chunk_index = size / CHUNK_SIZE;
             if new_tail_size > 0 {
                 let mut buf: ChunkBuffer = [0; CHUNK_SIZE as usize];
-                let s = self.read_chunk(chunk_index, &mut buf)?;
+                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];
@@ -260,7 +299,8 @@
 
 impl<F: ReadByChunk + RandomWrite> ReadByChunk for VerifiedFileEditor<F> {
     fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
-        self.file.read_chunk(chunk_index, buf)
+        let merkle_tree = self.merkle_tree.read().unwrap();
+        self.read_backing_chunk_verified(chunk_index, buf, &merkle_tree)
     }
 }
 
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 7df3b3e..e7084ad 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -265,8 +265,8 @@
         assertTrue(copyFileOnMicrodroid(srcPath, destPath));
 
         // Action
-        // Tampering with the first 2 4K block of the backing file.
-        sAndroid.run("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
+        // Tampering with the first 2 4K-blocks of the backing file.
+        zeroizeFileOnAndroid(backendPath, /* size */ 8192, /* offset */ 0);
 
         // Verify
         // Write to a block partially requires a read back to calculate the new hash. It should fail
@@ -289,6 +289,52 @@
     }
 
     @Test
+    public void testReadFailedIfDetectsTampering() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST);
+
+        String srcPath = "/system/bin/linker64";
+        String destPath = MOUNT_DIR + "/3";
+        String backendPath = TEST_OUTPUT_DIR + "/out.file";
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+
+        // Action
+        // Tampering with the first 4K-block of the backing file.
+        zeroizeFileOnAndroid(backendPath, /* size */ 4096, /* offset */ 0);
+
+        // Verify
+        // Force dropping the page cache, so that the next read can be validated.
+        runOnMicrodroid("echo 1 > /proc/sys/vm/drop_caches");
+        // A read will fail if the backing data has been tampered.
+        assertFalse(checkReadAtFileOffsetOnMicrodroid(
+                destPath, /* offset */ 0, /* number */ 4096));
+        assertTrue(checkReadAtFileOffsetOnMicrodroid(
+                destPath, /* offset */ 4096, /* number */ 4096));
+    }
+
+    @Test
+    public void testResizeFailedIfDetectsTampering() throws Exception {
+        // Setup
+        runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST);
+
+        String outputPath = MOUNT_DIR + "/3";
+        String backendPath = TEST_OUTPUT_DIR + "/out.file";
+        createFileWithOnesOnMicrodroid(outputPath, 8192);
+
+        // Action
+        // Tampering with the last 4K-block of the backing file.
+        zeroizeFileOnAndroid(backendPath, /* size */ 1, /* offset */ 4096);
+
+        // Verify
+        // A resize (to a non-multiple of 4K) will fail if the last backing chunk has been
+        // tampered. The original data is necessary (and has to be verified) to calculate the new
+        // hash with shorter data.
+        assertFalse(resizeFileOnMicrodroid(outputPath, 8000));
+    }
+
+    @Test
     public void testFileResize() throws Exception {
         // Setup
         runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
@@ -304,14 +350,14 @@
                 backendPath,
                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
 
-        resizeFileOnMicrodroid(outputPath, 15000);
+        assertTrue(resizeFileOnMicrodroid(outputPath, 15000));
         assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000);
         expectBackingFileConsistency(
                 outputPath,
                 backendPath,
                 "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
 
-        resizeFileOnMicrodroid(outputPath, 5000);
+        assertTrue(resizeFileOnMicrodroid(outputPath, 5000));
         assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000);
         expectBackingFileConsistency(
                 outputPath,
@@ -340,7 +386,7 @@
                 "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
 
         // Regular file operations work, e.g. resize.
-        resizeFileOnMicrodroid(authfsPath, 15000);
+        assertTrue(resizeFileOnMicrodroid(authfsPath, 15000));
         assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 15000);
         expectBackingFileConsistency(
                 authfsPath,
@@ -698,8 +744,9 @@
         assertEquals("Inconsistent mode for " + androidPath + " (android)", expected, actual);
     }
 
-    private void resizeFileOnMicrodroid(String path, long size) {
-        runOnMicrodroid("truncate -c -s " + size + " " + path);
+    private boolean resizeFileOnMicrodroid(String path, long size) {
+        CommandResult result = runOnMicrodroidForResult("truncate -c -s " + size + " " + path);
+        return result.getStatus() == CommandStatus.SUCCESS;
     }
 
     private long getFileSizeInBytesOnMicrodroid(String path) {
@@ -711,12 +758,22 @@
                 "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath);
     }
 
-    private boolean writeZerosAtFileOffsetOnMicrodroid(
-            String filePath, long offset, long numberOfZeros, boolean writeThrough) {
-        String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros;
+    private boolean checkReadAtFileOffsetOnMicrodroid(String filePath, long offset, long size) {
+        String cmd = "dd if=" + filePath + " of=/dev/null bs=1 count=" + size;
         if (offset > 0) {
             cmd += " skip=" + offset;
         }
+        CommandResult result = runOnMicrodroidForResult(cmd);
+        return result.getStatus() == CommandStatus.SUCCESS;
+    }
+
+    private boolean writeZerosAtFileOffsetOnMicrodroid(
+            String filePath, long offset, long numberOfZeros, boolean writeThrough) {
+        String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros
+                + " conv=notrunc";
+        if (offset > 0) {
+            cmd += " seek=" + offset;
+        }
         if (writeThrough) {
             cmd += " direct";
         }
@@ -724,6 +781,12 @@
         return result.getStatus() == CommandStatus.SUCCESS;
     }
 
+    private void zeroizeFileOnAndroid(String filePath, long size, long offset)
+            throws DeviceNotAvailableException {
+        sAndroid.run("dd if=/dev/zero of=" + filePath + " bs=1 count=" + size + " conv=notrunc"
+                + " seek=" + offset);
+    }
+
     private void runAuthFsOnMicrodroid(String flags) {
         String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;