diff options
| author | 2021-02-10 00:40:06 +0000 | |
|---|---|---|
| committer | 2021-02-10 00:40:06 +0000 | |
| commit | f26c5a51b5492d0513574f823635eecacd25ee04 (patch) | |
| tree | 57d27f9a3cf9af9c7e479c638d1fc7ef2b8803bd | |
| parent | a2409ede6671424d067e02ec618df7e65f52cee2 (diff) | |
| parent | 45c6231908265da0b91ad61df81a0c5af77e0c4a (diff) | |
Merge "Add test for font crash protection." into sc-dev
7 files changed, 212 insertions, 26 deletions
diff --git a/tests/ApkVerityTest/Android.bp b/tests/ApkVerityTest/Android.bp index 39dc9c286d5f..e2d2ecaf1684 100644 --- a/tests/ApkVerityTest/Android.bp +++ b/tests/ApkVerityTest/Android.bp @@ -16,7 +16,10 @@ java_test_host { name: "ApkVerityTest", srcs: ["src/**/*.java"], libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"], - static_libs: ["frameworks-base-hostutils"], + static_libs: [ + "block_device_writer_jar", + "frameworks-base-hostutils", + ], test_suites: ["general-tests", "vts"], target_required: [ "block_device_writer_module", diff --git a/tests/ApkVerityTest/block_device_writer/Android.bp b/tests/ApkVerityTest/block_device_writer/Android.bp index 37fbc29470f6..8f2d4bc70fa0 100644 --- a/tests/ApkVerityTest/block_device_writer/Android.bp +++ b/tests/ApkVerityTest/block_device_writer/Android.bp @@ -51,3 +51,9 @@ cc_test { test_suites: ["general-tests", "pts", "vts"], gtest: false, } + +java_library_host { + name: "block_device_writer_jar", + srcs: ["src/**/*.java"], + libs: ["tradefed", "junit"], +} diff --git a/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java b/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java new file mode 100644 index 000000000000..5c2c15b22bb0 --- /dev/null +++ b/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java @@ -0,0 +1,100 @@ +/* + * 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. + */ + +package com.android.blockdevicewriter; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import java.util.ArrayList; + +/** + * Wrapper for block_device_writer command. + * + * <p>To use this class, please push block_device_writer binary to /data/local/tmp. + * 1. In Android.bp, add: + * <pre> + * target_required: ["block_device_writer_module"], + * </pre> + * 2. In AndroidText.xml, add: + * <pre> + * <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + * <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> + * </target_preparer> + * </pre> + */ +public final class BlockDeviceWriter { + private static final String EXECUTABLE = "/data/local/tmp/block_device_writer"; + + /** + * Modifies a byte of the file directly against the backing block storage. + * + * The effect can only be observed when the page cache is read from disk again. See + * {@link #dropCaches} for details. + */ + public static void damageFileAgainstBlockDevice(ITestDevice device, String path, + long offsetOfTargetingByte) + throws DeviceNotAvailableException { + assertThat(path).startsWith("/data/"); + ITestDevice.MountPointInfo mountPoint = device.getMountPointInfo("/data"); + ArrayList<String> args = new ArrayList<>(); + args.add(EXECUTABLE); + if ("f2fs".equals(mountPoint.type)) { + args.add("--use-f2fs-pinning"); + } + args.add(mountPoint.filesystem); + args.add(path); + args.add(Long.toString(offsetOfTargetingByte)); + CommandResult result = device.executeShellV2Command(String.join(" ", args)); + assertWithMessage( + String.format("stdout=%s\nstderr=%s", result.getStdout(), result.getStderr())) + .that(result.getStatus()).isEqualTo(CommandStatus.SUCCESS); + } + + /** + * Drops file caches so that the result of {@link #damageFileAgainstBlockDevice} can be + * observed. If a process has an open FD or memory map of the damaged file, cache eviction won't + * happen and the damage cannot be observed. + */ + public static void dropCaches(ITestDevice device) throws DeviceNotAvailableException { + CommandResult result = device.executeShellV2Command( + "sync && echo 1 > /proc/sys/vm/drop_caches"); + assertThat(result.getStatus()).isEqualTo(CommandStatus.SUCCESS); + } + + public static void assertFileNotOpen(ITestDevice device, String path) + throws DeviceNotAvailableException { + CommandResult result = device.executeShellV2Command("lsof " + path); + assertThat(result.getStatus()).isEqualTo(CommandStatus.SUCCESS); + assertThat(result.getStdout()).isEmpty(); + } + + /** + * Checks if the give offset of a file can be read. + * This method will return false if the file has fs-verity enabled and is damaged at the offset. + */ + public static boolean canReadByte(ITestDevice device, String filePath, long offset) + throws DeviceNotAvailableException { + CommandResult result = device.executeShellV2Command( + "dd if=" + filePath + " bs=1 count=1 skip=" + Long.toString(offset)); + return result.getStatus() == CommandStatus.SUCCESS; + } +} diff --git a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java index d0eb9befbdee..ab3572ba2173 100644 --- a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java +++ b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.fail; import android.platform.test.annotations.RootPermissionTest; +import com.android.blockdevicewriter.BlockDeviceWriter; import com.android.fsverity.AddFsVerityCertRule; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.ITestDevice; @@ -334,22 +335,23 @@ public class ApkVerityTest extends BaseHostJUnit4Test { long offsetFirstByte = 0; // The first two pages should be both readable at first. - assertTrue(canReadByte(apkPath, offsetFirstByte)); + assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte)); if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { - assertTrue(canReadByte(apkPath, offsetFirstByte + FSVERITY_PAGE_SIZE)); + assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, + offsetFirstByte + FSVERITY_PAGE_SIZE)); } // Damage the file directly against the block device. damageFileAgainstBlockDevice(apkPath, offsetFirstByte); // Expect actual read from disk to fail but only at damaged page. - dropCaches(); - assertFalse(canReadByte(apkPath, offsetFirstByte)); + BlockDeviceWriter.dropCaches(mDevice); + assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte)); if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { long lastByteOfTheSamePage = offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1; - assertFalse(canReadByte(apkPath, lastByteOfTheSamePage)); - assertTrue(canReadByte(apkPath, lastByteOfTheSamePage + 1)); + assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage)); + assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage + 1)); } } @@ -362,21 +364,22 @@ public class ApkVerityTest extends BaseHostJUnit4Test { long offsetOfLastByte = apkSize - 1; // The first two pages should be both readable at first. - assertTrue(canReadByte(apkPath, offsetOfLastByte)); + assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte)); if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { - assertTrue(canReadByte(apkPath, offsetOfLastByte - FSVERITY_PAGE_SIZE)); + assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, + offsetOfLastByte - FSVERITY_PAGE_SIZE)); } // Damage the file directly against the block device. damageFileAgainstBlockDevice(apkPath, offsetOfLastByte); // Expect actual read from disk to fail but only at damaged page. - dropCaches(); - assertFalse(canReadByte(apkPath, offsetOfLastByte)); + BlockDeviceWriter.dropCaches(mDevice); + assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte)); if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE; - assertFalse(canReadByte(apkPath, firstByteOfTheSamePage)); - assertTrue(canReadByte(apkPath, firstByteOfTheSamePage - 1)); + assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage)); + assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage - 1)); } } @@ -395,8 +398,8 @@ public class ApkVerityTest extends BaseHostJUnit4Test { // from filesystem cache. Forcing GC workarounds the problem. int retry = 5; for (; retry > 0; retry--) { - dropCaches(); - if (!canReadByte(path, kTargetOffset)) { + BlockDeviceWriter.dropCaches(mDevice); + if (!BlockDeviceWriter.canReadByte(mDevice, path, kTargetOffset)) { break; } try { @@ -451,16 +454,6 @@ public class ApkVerityTest extends BaseHostJUnit4Test { return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim()); } - private void dropCaches() throws DeviceNotAvailableException { - expectRemoteCommandToSucceed("sync && echo 1 > /proc/sys/vm/drop_caches"); - } - - private boolean canReadByte(String filePath, long offset) throws DeviceNotAvailableException { - CommandResult result = mDevice.executeShellV2Command( - "dd if=" + filePath + " bs=1 count=1 skip=" + Long.toString(offset)); - return result.getStatus() == CommandStatus.SUCCESS; - } - private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException { CommandResult result = mDevice.executeShellV2Command(cmd); assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS, diff --git a/tests/UpdatableSystemFontTest/Android.bp b/tests/UpdatableSystemFontTest/Android.bp index d809fe8dad56..43a5078c3c24 100644 --- a/tests/UpdatableSystemFontTest/Android.bp +++ b/tests/UpdatableSystemFontTest/Android.bp @@ -16,8 +16,14 @@ java_test_host { name: "UpdatableSystemFontTest", srcs: ["src/**/*.java"], libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"], - static_libs: ["frameworks-base-hostutils"], + static_libs: [ + "block_device_writer_jar", + "frameworks-base-hostutils", + ], test_suites: ["general-tests", "vts"], + target_required: [ + "block_device_writer_module", + ], data: [ ":NotoColorEmojiTtf", ":UpdatableSystemFontTestCertDer", diff --git a/tests/UpdatableSystemFontTest/AndroidTest.xml b/tests/UpdatableSystemFontTest/AndroidTest.xml index efe5d703880c..7b919bd4b114 100644 --- a/tests/UpdatableSystemFontTest/AndroidTest.xml +++ b/tests/UpdatableSystemFontTest/AndroidTest.xml @@ -21,6 +21,7 @@ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> + <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> <option name="push" value="UpdatableSystemFontTestCert.der->/data/local/tmp/UpdatableSystemFontTestCert.der" /> <option name="push" value="NotoColorEmoji.ttf->/data/local/tmp/NotoColorEmoji.ttf" /> <option name="push" value="UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig" /> diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java index 6d161a5d7b3a..e249f8a99b0c 100644 --- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java +++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java @@ -21,7 +21,9 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.platform.test.annotations.RootPermissionTest; +import com.android.blockdevicewriter.BlockDeviceWriter; import com.android.fsverity.AddFsVerityCertRule; +import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; @@ -34,6 +36,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -126,6 +130,44 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test { TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG)); } + @Test + public void reboot() throws Exception { + expectRemoteCommandToSucceed(String.format("cmd font update %s %s", + TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG)); + String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); + assertThat(fontPath).startsWith("/data/fonts/files/"); + + expectRemoteCommandToSucceed("stop"); + expectRemoteCommandToSucceed("start"); + waitUntilFontCommandIsReady(); + String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF); + assertThat(fontPathAfterReboot).isEqualTo(fontPath); + } + + @Test + public void reboot_clearDamagedFiles() throws Exception { + expectRemoteCommandToSucceed(String.format("cmd font update %s %s", + TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG)); + String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); + assertThat(fontPath).startsWith("/data/fonts/files/"); + assertThat(BlockDeviceWriter.canReadByte(getDevice(), fontPath, 0)).isTrue(); + + BlockDeviceWriter.damageFileAgainstBlockDevice(getDevice(), fontPath, 0); + expectRemoteCommandToSucceed("stop"); + // We have to make sure system_server is gone before dropping caches, because system_server + // process holds font memory maps and prevents cache eviction. + waitUntilSystemServerIsGone(); + BlockDeviceWriter.assertFileNotOpen(getDevice(), fontPath); + BlockDeviceWriter.dropCaches(getDevice()); + assertThat(BlockDeviceWriter.canReadByte(getDevice(), fontPath, 0)).isFalse(); + + expectRemoteCommandToSucceed("start"); + waitUntilFontCommandIsReady(); + String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF); + assertWithMessage("Damaged file should be deleted") + .that(fontPathAfterReboot).startsWith("/system"); + } + private String getFontPath(String fontFileName) throws Exception { // TODO: add a dedicated command for testing. String lines = expectRemoteCommandToSucceed("cmd font dump"); @@ -153,4 +195,39 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test { .that(result.getStatus()) .isNotEqualTo(CommandStatus.SUCCESS); } + + private void waitUntilFontCommandIsReady() { + waitUntil(TimeUnit.SECONDS.toMillis(30), () -> { + try { + return getDevice().executeShellV2Command("cmd font status").getStatus() + == CommandStatus.SUCCESS; + } catch (DeviceNotAvailableException e) { + return false; + } + }); + } + + private void waitUntilSystemServerIsGone() { + waitUntil(TimeUnit.SECONDS.toMillis(30), () -> { + try { + return getDevice().executeShellV2Command("pid system_server").getStatus() + == CommandStatus.FAILED; + } catch (DeviceNotAvailableException e) { + return false; + } + }); + } + + private void waitUntil(long timeoutMillis, Supplier<Boolean> func) { + long untilMillis = System.currentTimeMillis() + timeoutMillis; + do { + if (func.get()) return; + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new AssertionError("Interrupted", e); + } + } while (System.currentTimeMillis() < untilMillis); + throw new AssertionError("Timed out"); + } } |