Revert "Merge AuthFsHostTest into MicrodroidTestCase"

This reverts commit 7fc99e6c5e03bccc7d461a86c46b1a03b34a547b.

Reason for revert: Affects MicrodroidHostTestCases. b/193749869

Change-Id: Ic72353d85427264845cf04408b256b74882cdc66
diff --git a/authfs/tests/Android.bp b/authfs/tests/Android.bp
new file mode 100644
index 0000000..8061c56
--- /dev/null
+++ b/authfs/tests/Android.bp
@@ -0,0 +1,21 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+    name: "AuthFsHostTest",
+    srcs: ["java/**/*.java"],
+    libs: [
+        "tradefed",
+        "compatibility-tradefed",
+        "compatibility-host-util",
+    ],
+    static_libs: [
+        "VirtualizationTestHelper",
+    ],
+    test_suites: ["general-tests"],
+    data: [
+        ":authfs_test_files",
+        ":MicrodroidTestApp.signed",
+    ],
+}
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
new file mode 100644
index 0000000..8f940f6
--- /dev/null
+++ b/authfs/tests/AndroidTest.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Config for authfs tests">
+    <!-- Need root to start virtualizationservice -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <!-- virtualizationservice doesn't have access to shell_data_file. Instead of giving it
+          a test-only permission, run it without selinux -->
+    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
+
+    <!-- Basic checks that the device has all the prerequisites. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="throw-if-cmd-fail" value="true" />
+        <!-- Make sure kernel has FUSE enabled. -->
+        <option name="run-command" value="ls /dev/fuse" />
+        <!-- Make sure necessary executables are installed. -->
+        <option name="run-command" value="ls /apex/com.android.virt/bin/fd_server" />
+        <option name="run-command" value="ls /apex/com.android.virt/bin/authfs" />
+        <!-- Prepare test directory. -->
+        <option name="run-command" value="mkdir -p /data/local/tmp/authfs/mnt" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/authfs" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="abort-on-push-failure" value="true" />
+        <option name="push-file" key="cert.der" value="/data/local/tmp/authfs/cert.der" />
+        <option name="push-file" key="input.4m" value="/data/local/tmp/authfs/input.4m" />
+        <option name="push-file" key="input.4k1" value="/data/local/tmp/authfs/input.4k1" />
+        <option name="push-file" key="input.4k" value="/data/local/tmp/authfs/input.4k" />
+        <option name="push-file" key="input.4m.fsv_sig"
+            value="/data/local/tmp/authfs/input.4m.fsv_sig" />
+        <option name="push-file" key="input.4k1.fsv_sig"
+            value="/data/local/tmp/authfs/input.4k1.fsv_sig" />
+        <option name="push-file" key="input.4k.fsv_sig"
+            value="/data/local/tmp/authfs/input.4k.fsv_sig" />
+        <option name="push-file" key="input.4m.merkle_dump"
+            value="/data/local/tmp/authfs/input.4m.merkle_dump" />
+        <option name="push-file" key="input.4m.merkle_dump.bad"
+            value="/data/local/tmp/authfs/input.4m.merkle_dump.bad" />
+        <option name="push-file" key="input.4k1.merkle_dump"
+            value="/data/local/tmp/authfs/input.4k1.merkle_dump" />
+        <option name="push-file" key="input.4k.merkle_dump"
+            value="/data/local/tmp/authfs/input.4k.merkle_dump" />
+    </target_preparer>
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="AuthFsHostTest.jar" />
+    </test>
+</configuration>
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
new file mode 100644
index 0000000..426b333
--- /dev/null
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.virt.fs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.RootPermissionTest;
+import android.virt.test.VirtualizationTestCaseBase;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandResult;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+// TODO move to Virtualization/tests/hostside/
+@RootPermissionTest
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class AuthFsHostTest extends VirtualizationTestCaseBase {
+
+    /** Test directory on Android where data are located */
+    private static final String TEST_DIR = "/data/local/tmp/authfs";
+
+    /** Mount point of authfs on Microdroid during the test */
+    private static final String MOUNT_DIR = "/data/local/tmp";
+
+    /** Path to fd_server on Android */
+    private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server";
+
+    /** Path to authfs on Microdroid */
+    private static final String AUTHFS_BIN = "/system/bin/authfs";
+
+    /** Plenty of time for authfs to get ready */
+    private static final int AUTHFS_INIT_TIMEOUT_MS = 1500;
+
+    /** FUSE's magic from statfs(2) */
+    private static final String FUSE_SUPER_MAGIC_HEX = "65735546";
+
+    private ExecutorService mThreadPool;
+    private String mCid;
+
+    @Before
+    public void setUp() throws DeviceNotAvailableException {
+        testIfDeviceIsCapable();
+
+        cleanUpTestFiles();
+
+        prepareVirtualizationTestSetup();
+
+        mThreadPool = Executors.newCachedThreadPool();
+
+        // For each test case, boot and adb connect to a new Microdroid
+        final String apkName = "MicrodroidTestApp.apk";
+        final String packageName = "com.android.microdroid.test";
+        final String configPath = "assets/vm_config.json"; // path inside the APK
+        mCid = startMicrodroid(apkName, packageName, configPath, /* debug */ false);
+        adbConnectToMicrodroid(mCid);
+
+        // Root because authfs (started from shell in this test) currently require root to open
+        // /dev/fuse and mount the FUSE.
+        rootMicrodroid();
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        if (mCid != null) {
+            shutdownMicrodroid(mCid);
+            mCid = null;
+        }
+
+        tryRunOnAndroid("killall fd_server");
+        cleanUpTestFiles();
+        cleanUpVirtualizationTestSetup();
+    }
+
+    private void cleanUpTestFiles() throws DeviceNotAvailableException {
+        tryRunOnAndroid("rm -f " + TEST_DIR + "/output");
+    }
+
+    @Test
+    public void testReadWithFsverityVerification_RemoteFile()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid(
+                "3<input.4m 4<input.4m.merkle_dump 5<input.4m.fsv_sig 6<input.4m",
+                "--ro-fds 3:4:5 --ro-fds 6 --rpc-binder");
+
+        runAuthFsOnMicrodroid(
+                "--remote-ro-file-unverified 10:6:4194304 --remote-ro-file 11:3:4194304:cert.der"
+                        + " --cid 2");
+
+        // Action
+        String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+        String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
+
+        // Verify
+        String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m");
+
+        assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4m, actualHashUnverified4m);
+        assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4m, actualHash4m);
+    }
+
+    // Separate the test from the above simply because exec in shell does not allow open too many
+    // files.
+    @Test
+    public void testReadWithFsverityVerification_RemoteSmallerFile()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid(
+                "3<input.4k 4<input.4k.merkle_dump 5<input.4k.fsv_sig"
+                        + " 6<input.4k1 7<input.4k1.merkle_dump 8<input.4k1.fsv_sig",
+                "--ro-fds 3:4:5 --ro-fds 6:7:8 --rpc-binder");
+        runAuthFsOnMicrodroid(
+                "--remote-ro-file 10:3:4096:cert.der --remote-ro-file 11:6:4097:cert.der --cid 2");
+
+        // Action
+        String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
+        String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
+
+        // Verify
+        String expectedHash4k = computeFileHashOnAndroid(TEST_DIR + "/input.4k");
+        String expectedHash4k1 = computeFileHashOnAndroid(TEST_DIR + "/input.4k1");
+
+        assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4k, actualHash4k);
+        assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4k1, actualHash4k1);
+    }
+
+    @Test
+    public void testReadWithFsverityVerification_TamperedMerkleTree()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid(
+                "3<input.4m 4<input.4m.merkle_dump.bad 5<input.4m.fsv_sig",
+                "--ro-fds 3:4:5 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-ro-file 10:3:4096:cert.der --cid 2");
+
+        // Verify
+        assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/10", "/dev/null"));
+    }
+
+    @Test
+    public void testWriteThroughCorrectly()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
+
+        // Action
+        String srcPath = "/system/bin/linker64";
+        String destPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+
+        // Verify
+        String expectedHash = computeFileHashOnMicrodroid(srcPath);
+        expectBackingFileConsistency(destPath, backendPath, expectedHash);
+    }
+
+    @Test
+    public void testWriteFailedIfDetectsTampering()
+            throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
+
+        String srcPath = "/system/bin/linker64";
+        String destPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+        assertTrue(copyFileOnMicrodroid(srcPath, destPath));
+
+        // Action
+        // Tampering with the first 2 4K block of the backing file.
+        runOnAndroid("dd if=/dev/zero of=" + backendPath + " bs=1 count=8192");
+
+        // Verify
+        // Write to a block partially requires a read back to calculate the new hash. It should fail
+        // when the content is inconsistent to the known hash. Use direct I/O to avoid simply
+        // writing to the filesystem cache.
+        assertEquals(
+                tryRunOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 direct"),
+                null);
+
+        // A full 4K write does not require to read back, so write can succeed even if the backing
+        // block has already been tampered.
+        runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
+
+        // Otherwise, a partial write with correct backing file should still succeed.
+        runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
+    }
+
+    @Test
+    public void testFileResize() throws DeviceNotAvailableException, InterruptedException {
+        // Setup
+        runFdServerOnAndroid("3<>output", "--rw-fds 3 --rpc-binder");
+        runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid 2");
+        String outputPath = MOUNT_DIR + "/20";
+        String backendPath = TEST_DIR + "/output";
+
+        // Action & Verify
+        runOnMicrodroid("yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000);
+        expectBackingFileConsistency(
+                outputPath,
+                backendPath,
+                "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
+
+        resizeFileOnMicrodroid(outputPath, 15000);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 15000);
+        expectBackingFileConsistency(
+                outputPath,
+                backendPath,
+                "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
+
+        resizeFileOnMicrodroid(outputPath, 5000);
+        assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 5000);
+        expectBackingFileConsistency(
+                outputPath,
+                backendPath,
+                "e53130831c13dabff71d5d1797e3aaa467b4b7d32b3b8782c4ff03d76976f2aa");
+    }
+
+    private void expectBackingFileConsistency(
+            String authFsPath, String backendPath, String expectedHash)
+            throws DeviceNotAvailableException {
+        String hashOnAuthFs = computeFileHashOnMicrodroid(authFsPath);
+        assertEquals("File hash is different to expectation", expectedHash, hashOnAuthFs);
+
+        String hashOfBackingFile = computeFileHashOnAndroid(backendPath);
+        assertEquals(
+                "Inconsistent file hash on the backend storage", hashOnAuthFs, hashOfBackingFile);
+    }
+
+    private String computeFileHashOnMicrodroid(String path) {
+        String result = runOnMicrodroid("sha256sum " + path);
+        String[] tokens = result.split("\\s");
+        if (tokens.length > 0) {
+            return tokens[0];
+        } else {
+            CLog.e("Unrecognized output by sha256sum: " + result);
+            return "";
+        }
+    }
+
+    private boolean copyFileOnMicrodroid(String src, String dest)
+            throws DeviceNotAvailableException {
+        // TODO(b/182576497): cp returns error because close(2) returns ENOSYS in the current authfs
+        // implementation. We should probably fix that since programs can expect close(2) return 0.
+        String cmd = "cat " + src + " > " + dest;
+        return tryRunOnMicrodroid(cmd) != null;
+    }
+
+    private String computeFileHashOnAndroid(String path) throws DeviceNotAvailableException {
+        String result = runOnAndroid("sha256sum " + path);
+        String[] tokens = result.split("\\s");
+        if (tokens.length > 0) {
+            return tokens[0];
+        } else {
+            CLog.e("Unrecognized output by sha256sum: " + result);
+            return "";
+        }
+    }
+
+    private void resizeFileOnMicrodroid(String path, long size) {
+        runOnMicrodroid("truncate -c -s " + size + " " + path);
+    }
+
+    private long getFileSizeInBytesOnMicrodroid(String path) {
+        return Long.parseLong(runOnMicrodroid("stat -c '%s' " + path));
+    }
+
+    private void runAuthFsOnMicrodroid(String flags) {
+        String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
+
+        mThreadPool.submit(
+                () -> {
+                    CLog.i("Starting authfs");
+                    CommandResult result = runOnMicrodroidForResult(cmd);
+                    CLog.w("authfs has stopped: " + result);
+                });
+        try {
+            PollingCheck.waitFor(
+                    AUTHFS_INIT_TIMEOUT_MS, () -> isMicrodroidDirectoryOnFuse(MOUNT_DIR));
+        } catch (Exception e) {
+            // Convert the broad Exception into an unchecked exception to avoid polluting all other
+            // methods. waitFor throws Exception because the callback, Callable#call(), has a
+            // signature to throw an Exception.
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void runFdServerOnAndroid(String execParamsForOpeningFds, String flags)
+            throws DeviceNotAvailableException {
+        String cmd = "cd " + TEST_DIR + " && exec " + execParamsForOpeningFds + " " + FD_SERVER_BIN
+                + " " + flags;
+        mThreadPool.submit(
+                () -> {
+                    try {
+                        CLog.i("Starting fd_server");
+                        CommandResult result = getDevice().executeShellV2Command(cmd);
+                        CLog.w("fd_server has stopped: " + result);
+                    } catch (DeviceNotAvailableException e) {
+                        CLog.e("Error running fd_server", e);
+                        throw new RuntimeException(e);
+                    }
+                });
+    }
+
+    private boolean isMicrodroidDirectoryOnFuse(String path) {
+        String fs_type = tryRunOnMicrodroid("stat -f -c '%t' " + path);
+        return FUSE_SUPER_MAGIC_HEX.equals(fs_type);
+    }
+}