Loading tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java +43 −20 Original line number Diff line number Diff line Loading @@ -21,7 +21,10 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; import android.os.Bundle; import android.security.FileIntegrityManager; import android.system.Os; import android.system.OsConstants; import android.util.Log; import androidx.test.core.app.ApplicationProvider; Loading @@ -35,8 +38,9 @@ import org.junit.Test; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * Test helper that works with the host-side test to set up a test file, and to verify fs-verity Loading @@ -47,8 +51,6 @@ public class Helper { private static final String FILENAME = "test.file"; private static final long BLOCK_SIZE = 4096; @Rule public final AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( Loading @@ -58,7 +60,7 @@ public class Helper { @Test public void prepareTest() throws Exception { Context context = ApplicationProvider.getApplicationContext(); android.os.Bundle testArgs = InstrumentationRegistry.getArguments(); Bundle testArgs = InstrumentationRegistry.getArguments(); String basename = testArgs.getString("basename"); context.deleteFile(basename); Loading @@ -84,31 +86,52 @@ public class Helper { fim.setupFsVerity(context.getFileStreamPath(basename)); } private static long getPageSize() { String arch = System.getProperty("os.arch"); Log.d(TAG, "os.arch=" + arch); if ("x86_64".equals(arch)) { // Override the fake 16K page size from cf_x86_64_pgagnostic. The real page size on // x86_64 is always 4K. This test needs the real page size because it is testing I/O // error reporting behavior that is dependent on the real page size. return 4096; } return Os.sysconf(OsConstants._SC_PAGE_SIZE); } @Test public void verifyFileRead() throws Exception { Context context = ApplicationProvider.getApplicationContext(); // Collect indices that the backing blocks are supposed to be corrupted. android.os.Bundle testArgs = InstrumentationRegistry.getArguments(); Bundle testArgs = InstrumentationRegistry.getArguments(); assertThat(testArgs).isNotNull(); String filePath = testArgs.getString("filePath"); String csv = testArgs.getString("brokenBlockIndicesCsv"); Log.d(TAG, "brokenBlockIndicesCsv: " + csv); String[] strings = csv.split(","); var corrupted = new ArrayList(strings.length); for (int i = 0; i < strings.length; i++) { corrupted.add(Integer.parseInt(strings[i])); String csv = testArgs.getString("brokenByteIndicesCsv"); Log.d(TAG, "brokenByteIndicesCsv: " + csv); // Build the set of pages that contain a corrupted byte. final long pageSize = getPageSize(); Set<Long> corruptedPageIndices = new HashSet(); for (String s : csv.split(",")) { long byteIndex = Long.parseLong(s); long pageIndex = byteIndex / pageSize; corruptedPageIndices.add(pageIndex); } // Expect the read to succeed or fail per the prior. try (var file = new RandomAccessFile(filePath, "r")) { long total_blocks = (file.length() + BLOCK_SIZE - 1) / BLOCK_SIZE; for (int i = 0; i < (int) total_blocks; i++) { file.seek(i * BLOCK_SIZE); if (corrupted.contains(i)) { Log.d(TAG, "Expecting read at block #" + i + " to fail"); Log.d(TAG, "corruptedPageIndices=" + corruptedPageIndices); // Read bytes from the file and verify the expected result based on the containing page. // (The kernel reports fs-verity errors at page granularity.) final long stride = 1024; // Using a stride that is a divisor of the page size ensures that the last page is tested. assertThat(pageSize % stride).isEqualTo(0); try (RandomAccessFile file = new RandomAccessFile(filePath, "r")) { for (long byteIndex = 0; byteIndex < file.length(); byteIndex += stride) { file.seek(byteIndex); long pageIndex = byteIndex / pageSize; if (corruptedPageIndices.contains(pageIndex)) { Log.d(TAG, "Expecting read at byte #" + byteIndex + " to fail"); assertThrows(IOException.class, () -> file.read()); } else { Log.d(TAG, "Expecting read at byte #" + byteIndex + " to succeed"); assertThat(file.readByte()).isEqualTo('1'); } } Loading tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java +13 −8 Original line number Diff line number Diff line Loading @@ -33,13 +33,12 @@ import org.junit.runner.RunWith; /** * This test verifies fs-verity works end-to-end. There is a corresponding helper app. * * <p>The helper app uses a FileIntegrityManager API to enable fs-verity to a file. The host test * here * tampers with the file's backing storage, then tells the helper app to read and expect * <p>The helper app uses a FileIntegrityManager API to enable fs-verity on a file. The host test * here tampers with the file's backing storage, then tells the helper app to read and expect * success/failure on read. * * <p>In order to make sure a block of the file is readable only if the underlying block on disk * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical * address against the block device. * <p>Since the filesystem by design provides no way to corrupt fs-verity files itself, the test * needs to bypass the filesystem and write directly to the block device to corrupt the files. */ @RootPermissionTest @RunWith(DeviceJUnit4ClassRunner.class) Loading @@ -57,7 +56,7 @@ public class FsVerityHostTest extends BaseHostJUnit4Test { BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 8192); BlockDeviceWriter.dropCaches(device); verifyRead(getTargetFilePath(), "0,2"); verifyRead(getTargetFilePath(), "0,8192"); } @Test Loading @@ -70,7 +69,7 @@ public class FsVerityHostTest extends BaseHostJUnit4Test { BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 128 * 4096 + 1); BlockDeviceWriter.dropCaches(device); verifyRead(getTargetFilePath(), "1,100,128"); verifyRead(getTargetFilePath(), "4096,409600,524289"); } private String getTargetFilePath() throws DeviceNotAvailableException { Loading @@ -87,11 +86,17 @@ public class FsVerityHostTest extends BaseHostJUnit4Test { assertThat(runDeviceTests(options)).isTrue(); } /** * Verifies the read success/failure expectation given the corrupted byte indices in the file. * * @param path the remote file path to read. * @param indicesCsv a comma-separated list of indices of bytes that were corrupted. */ private void verifyRead(String path, String indicesCsv) throws Exception { DeviceTestRunOptions options = new DeviceTestRunOptions(TARGET_PACKAGE); options.setTestClassName(TARGET_PACKAGE + ".Helper"); options.setTestMethodName("verifyFileRead"); options.addInstrumentationArg("brokenBlockIndicesCsv", indicesCsv); options.addInstrumentationArg("brokenByteIndicesCsv", indicesCsv); options.addInstrumentationArg("filePath", getTargetFilePath()); assertThat(runDeviceTests(options)).isTrue(); } Loading Loading
tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java +43 −20 Original line number Diff line number Diff line Loading @@ -21,7 +21,10 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; import android.os.Bundle; import android.security.FileIntegrityManager; import android.system.Os; import android.system.OsConstants; import android.util.Log; import androidx.test.core.app.ApplicationProvider; Loading @@ -35,8 +38,9 @@ import org.junit.Test; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * Test helper that works with the host-side test to set up a test file, and to verify fs-verity Loading @@ -47,8 +51,6 @@ public class Helper { private static final String FILENAME = "test.file"; private static final long BLOCK_SIZE = 4096; @Rule public final AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( Loading @@ -58,7 +60,7 @@ public class Helper { @Test public void prepareTest() throws Exception { Context context = ApplicationProvider.getApplicationContext(); android.os.Bundle testArgs = InstrumentationRegistry.getArguments(); Bundle testArgs = InstrumentationRegistry.getArguments(); String basename = testArgs.getString("basename"); context.deleteFile(basename); Loading @@ -84,31 +86,52 @@ public class Helper { fim.setupFsVerity(context.getFileStreamPath(basename)); } private static long getPageSize() { String arch = System.getProperty("os.arch"); Log.d(TAG, "os.arch=" + arch); if ("x86_64".equals(arch)) { // Override the fake 16K page size from cf_x86_64_pgagnostic. The real page size on // x86_64 is always 4K. This test needs the real page size because it is testing I/O // error reporting behavior that is dependent on the real page size. return 4096; } return Os.sysconf(OsConstants._SC_PAGE_SIZE); } @Test public void verifyFileRead() throws Exception { Context context = ApplicationProvider.getApplicationContext(); // Collect indices that the backing blocks are supposed to be corrupted. android.os.Bundle testArgs = InstrumentationRegistry.getArguments(); Bundle testArgs = InstrumentationRegistry.getArguments(); assertThat(testArgs).isNotNull(); String filePath = testArgs.getString("filePath"); String csv = testArgs.getString("brokenBlockIndicesCsv"); Log.d(TAG, "brokenBlockIndicesCsv: " + csv); String[] strings = csv.split(","); var corrupted = new ArrayList(strings.length); for (int i = 0; i < strings.length; i++) { corrupted.add(Integer.parseInt(strings[i])); String csv = testArgs.getString("brokenByteIndicesCsv"); Log.d(TAG, "brokenByteIndicesCsv: " + csv); // Build the set of pages that contain a corrupted byte. final long pageSize = getPageSize(); Set<Long> corruptedPageIndices = new HashSet(); for (String s : csv.split(",")) { long byteIndex = Long.parseLong(s); long pageIndex = byteIndex / pageSize; corruptedPageIndices.add(pageIndex); } // Expect the read to succeed or fail per the prior. try (var file = new RandomAccessFile(filePath, "r")) { long total_blocks = (file.length() + BLOCK_SIZE - 1) / BLOCK_SIZE; for (int i = 0; i < (int) total_blocks; i++) { file.seek(i * BLOCK_SIZE); if (corrupted.contains(i)) { Log.d(TAG, "Expecting read at block #" + i + " to fail"); Log.d(TAG, "corruptedPageIndices=" + corruptedPageIndices); // Read bytes from the file and verify the expected result based on the containing page. // (The kernel reports fs-verity errors at page granularity.) final long stride = 1024; // Using a stride that is a divisor of the page size ensures that the last page is tested. assertThat(pageSize % stride).isEqualTo(0); try (RandomAccessFile file = new RandomAccessFile(filePath, "r")) { for (long byteIndex = 0; byteIndex < file.length(); byteIndex += stride) { file.seek(byteIndex); long pageIndex = byteIndex / pageSize; if (corruptedPageIndices.contains(pageIndex)) { Log.d(TAG, "Expecting read at byte #" + byteIndex + " to fail"); assertThrows(IOException.class, () -> file.read()); } else { Log.d(TAG, "Expecting read at byte #" + byteIndex + " to succeed"); assertThat(file.readByte()).isEqualTo('1'); } } Loading
tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java +13 −8 Original line number Diff line number Diff line Loading @@ -33,13 +33,12 @@ import org.junit.runner.RunWith; /** * This test verifies fs-verity works end-to-end. There is a corresponding helper app. * * <p>The helper app uses a FileIntegrityManager API to enable fs-verity to a file. The host test * here * tampers with the file's backing storage, then tells the helper app to read and expect * <p>The helper app uses a FileIntegrityManager API to enable fs-verity on a file. The host test * here tampers with the file's backing storage, then tells the helper app to read and expect * success/failure on read. * * <p>In order to make sure a block of the file is readable only if the underlying block on disk * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical * address against the block device. * <p>Since the filesystem by design provides no way to corrupt fs-verity files itself, the test * needs to bypass the filesystem and write directly to the block device to corrupt the files. */ @RootPermissionTest @RunWith(DeviceJUnit4ClassRunner.class) Loading @@ -57,7 +56,7 @@ public class FsVerityHostTest extends BaseHostJUnit4Test { BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 8192); BlockDeviceWriter.dropCaches(device); verifyRead(getTargetFilePath(), "0,2"); verifyRead(getTargetFilePath(), "0,8192"); } @Test Loading @@ -70,7 +69,7 @@ public class FsVerityHostTest extends BaseHostJUnit4Test { BlockDeviceWriter.damageFileAgainstBlockDevice(device, getTargetFilePath(), 128 * 4096 + 1); BlockDeviceWriter.dropCaches(device); verifyRead(getTargetFilePath(), "1,100,128"); verifyRead(getTargetFilePath(), "4096,409600,524289"); } private String getTargetFilePath() throws DeviceNotAvailableException { Loading @@ -87,11 +86,17 @@ public class FsVerityHostTest extends BaseHostJUnit4Test { assertThat(runDeviceTests(options)).isTrue(); } /** * Verifies the read success/failure expectation given the corrupted byte indices in the file. * * @param path the remote file path to read. * @param indicesCsv a comma-separated list of indices of bytes that were corrupted. */ private void verifyRead(String path, String indicesCsv) throws Exception { DeviceTestRunOptions options = new DeviceTestRunOptions(TARGET_PACKAGE); options.setTestClassName(TARGET_PACKAGE + ".Helper"); options.setTestMethodName("verifyFileRead"); options.addInstrumentationArg("brokenBlockIndicesCsv", indicesCsv); options.addInstrumentationArg("brokenByteIndicesCsv", indicesCsv); options.addInstrumentationArg("filePath", getTargetFilePath()); assertThat(runDeviceTests(options)).isTrue(); } Loading