Merge "Fix explicit checks for /dev/kvm"
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 ff60d4a..5cd4af8 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -264,8 +264,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
@@ -288,6 +288,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");
@@ -303,14 +349,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,
@@ -339,7 +385,7 @@
"684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
// Regular file operations work, e.g. resize.
- resizeFileOnMicrodroid(authfsPath, 15000);
+ assertTrue(resizeFileOnMicrodroid(authfsPath, 15000));
assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 15000);
expectBackingFileConsistency(
authfsPath,
@@ -697,8 +743,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) {
@@ -710,12 +757,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";
}
@@ -723,6 +780,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;
diff --git a/tests/common.cc b/tests/common.cc
index 9602283..5d32351 100644
--- a/tests/common.cc
+++ b/tests/common.cc
@@ -14,14 +14,22 @@
* limitations under the License.
*/
+#include <android/sysprop/HypervisorProperties.sysprop.h>
+
#include "virt/VirtualizationTest.h"
+using android::sysprop::HypervisorProperties::hypervisor_protected_vm_supported;
+using android::sysprop::HypervisorProperties::hypervisor_vm_supported;
+
namespace {
bool isVmSupported() {
- const std::array<const char *, 4> needed_files = {
- "/dev/kvm",
- "/dev/vhost-vsock",
+ bool has_capability = hypervisor_vm_supported().value_or(false) ||
+ hypervisor_protected_vm_supported().value_or(false);
+ if (!has_capability) {
+ return false;
+ }
+ const std::array<const char *, 2> needed_files = {
"/apex/com.android.virt/bin/crosvm",
"/apex/com.android.virt/bin/virtualizationservice",
};