Merge "Add test for font crash protection." into sc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
f26c5a51b5
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user