[automerger skipped] Align /data partition rules with vts_fs_test. am: 59f2b11c48 am: 8a065645ab -s ours

am skip reason: Merged-In Iafaa370b732457bf1ba1dcabfb63c2490ad1f176 with SHA-1 b53eeb6133 is already in history

Original change: https://android-review.googlesource.com/c/platform/system/gsid/+/3048275

Change-Id: I0024b48b296dbcfcff46036c24bf432fc97e35ac
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 50618f9..1b78971 100644
--- a/Android.bp
+++ b/Android.bp
@@ -74,21 +74,19 @@
     recovery_available: true,
     vendor_available: true,
     export_include_dirs: ["include"],
+    apex_available: [
+        "//apex_available:anyapex",
+        "//apex_available:platform",
+    ],
+    min_sdk_version: "31",
 }
 
-cc_binary {
-    name: "gsid",
+cc_defaults {
+    name: "gsid_defaults",
     srcs: [
-        "daemon.cpp",
         "gsi_service.cpp",
         "partition_installer.cpp",
     ],
-    required: [
-        "mke2fs",
-    ],
-    init_rc: [
-        "gsid.rc",
-    ],
     shared_libs: [
         "libbase",
         "libbinder",
@@ -124,6 +122,22 @@
     local_include_dirs: ["include"],
 }
 
+cc_binary {
+    name: "gsid",
+    defaults: [
+        "gsid_defaults",
+    ],
+    srcs: [
+        "daemon.cpp",
+    ],
+    required: [
+        "mke2fs",
+    ],
+    init_rc: [
+        "gsid.rc",
+    ],
+}
+
 aidl_interface {
     name: "gsi_aidl_interface",
     unstable: true,
@@ -149,3 +163,22 @@
     ],
     path: "aidl",
 }
+
+cc_fuzz {
+    name: "gsi_service_fuzzer",
+    defaults: [
+        "gsid_defaults",
+        "service_fuzzer_defaults",
+        "fuzzer_disable_leaks",
+    ],
+    srcs: [
+        "fuzzers/GsiServiceFuzzer.cpp",
+    ],
+    fuzz_config: {
+        triage_assignee: "waghpawan@google.com",
+        cc: [
+            "elsk@google.com",
+            "yochiang@google.com",
+        ]
+    },
+}
diff --git a/OWNERS b/OWNERS
index a9d0479..e8bcfb2 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 30545
+# Bug component: 322150
 dvander@google.com
 sspatil@google.com
 elsk@google.com
diff --git a/fuzzers/GsiServiceFuzzer.cpp b/fuzzers/GsiServiceFuzzer.cpp
new file mode 100644
index 0000000..1d100d8
--- /dev/null
+++ b/fuzzers/GsiServiceFuzzer.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include <fuzzbinder/libbinder_driver.h>
+
+#include "gsi_service.h"
+
+using android::fuzzService;
+using android::sp;
+using android::gsi::GsiService;
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    auto binder = sp<GsiService>::make();
+    fuzzService(binder, FuzzedDataProvider(data, size));
+    return 0;
+}
\ No newline at end of file
diff --git a/gsi_service.cpp b/gsi_service.cpp
index 2caa445..224f520 100644
--- a/gsi_service.cpp
+++ b/gsi_service.cpp
@@ -109,6 +109,15 @@
         if (!status.isOk()) return status;                            \
     } while (0)
 
+#define ENFORCE_SYSTEM_OR_SHELL_IF_UNLOCK                            \
+    do {                                                             \
+        if (!android::base::EndsWith(GetActiveDsuSlot(), ".lock")) { \
+            ENFORCE_SYSTEM_OR_SHELL;                                 \
+        } else {                                                     \
+            ENFORCE_SYSTEM;                                          \
+        }                                                            \
+    } while (0)
+
 int GsiService::SaveInstallation(const std::string& installation) {
     auto dsu_slot = GetDsuSlot(installation);
     auto install_dir_file = DsuInstallDirFile(dsu_slot);
@@ -314,7 +323,7 @@
 
 binder::Status GsiService::enableGsiAsync(bool one_shot, const std::string& dsuSlot,
                                           const sp<IGsiServiceCallback>& resultCallback) {
-    ENFORCE_SYSTEM_OR_SHELL;
+    ENFORCE_SYSTEM_OR_SHELL_IF_UNLOCK;
     std::lock_guard<std::mutex> guard(lock_);
 
     const auto result = EnableGsi(one_shot, dsuSlot);
@@ -323,7 +332,7 @@
 }
 
 binder::Status GsiService::enableGsi(bool one_shot, const std::string& dsuSlot, int* _aidl_return) {
-    ENFORCE_SYSTEM_OR_SHELL;
+    ENFORCE_SYSTEM_OR_SHELL_IF_UNLOCK;
     std::lock_guard<std::mutex> guard(lock_);
 
     *_aidl_return = EnableGsi(one_shot, dsuSlot);
@@ -343,10 +352,11 @@
 }
 
 binder::Status GsiService::removeGsiAsync(const sp<IGsiServiceCallback>& resultCallback) {
-    bool result = false;
-    auto status = removeGsi(&result);
-    if (!status.isOk()) {
-        LOG(ERROR) << "Could not removeGsi: " << status.exceptionMessage().string();
+    int result = IGsiService::INSTALL_OK;
+    bool success = true;
+    auto status = removeGsi(&success);
+    if (!status.isOk() || !success) {
+        LOG(ERROR) << "Could not removeGsi: " << status.exceptionMessage().c_str();
         result = IGsiService::INSTALL_ERROR_GENERIC;
     }
     resultCallback->onResult(result);
@@ -354,7 +364,7 @@
 }
 
 binder::Status GsiService::removeGsi(bool* _aidl_return) {
-    ENFORCE_SYSTEM_OR_SHELL;
+    ENFORCE_SYSTEM_OR_SHELL_IF_UNLOCK;
     std::lock_guard<std::mutex> guard(lock_);
 
     std::string install_dir = GetActiveInstalledImageDir();
@@ -369,7 +379,7 @@
 }
 
 binder::Status GsiService::disableGsi(bool* _aidl_return) {
-    ENFORCE_SYSTEM_OR_SHELL;
+    ENFORCE_SYSTEM_OR_SHELL_IF_UNLOCK;
     std::lock_guard<std::mutex> guard(lock_);
 
     *_aidl_return = DisableGsiInstall();
@@ -436,7 +446,7 @@
 }
 
 binder::Status GsiService::zeroPartition(const std::string& name, int* _aidl_return) {
-    ENFORCE_SYSTEM_OR_SHELL;
+    ENFORCE_SYSTEM_OR_SHELL_IF_UNLOCK;
     std::lock_guard<std::mutex> guard(lock_);
 
     if (IsGsiRunning() || !IsGsiInstalled()) {
@@ -496,6 +506,12 @@
         return binder::Status::ok();
     }
     int fd = installer_->GetPartitionFd();
+    if (fd == -1) {
+        LOG(ERROR) << "Failed to get partition fd";
+        *_aidl_return = INSTALL_ERROR_GENERIC;
+        return binder::Status::ok();
+    }
+
     if (!GetAvbPublicKeyFromFd(fd, dst)) {
         LOG(ERROR) << "Failed to extract AVB public key";
         *_aidl_return = INSTALL_ERROR_GENERIC;
@@ -610,7 +626,7 @@
             auto status = on_progress->onProgress(static_cast<int64_t>(current),
                                                   static_cast<int64_t>(total));
             if (!status.isOk()) {
-                LOG(ERROR) << "progress callback returned: " << status.toString8().string();
+                LOG(ERROR) << "progress callback returned: " << status.toString8().c_str();
                 return false;
             }
             return true;
@@ -871,7 +887,8 @@
     }
 
     if (access(install_dir.c_str(), F_OK) != 0 && (errno == ENOENT)) {
-        if (android::base::StartsWith(install_dir, kDsuSDPrefix)) {
+        if (android::base::StartsWith(install_dir, kDsuSDPrefix) ||
+            android::base::StartsWith(install_dir, kDefaultDsuImageFolder)) {
             if (mkdir(install_dir.c_str(), 0755) != 0) {
                 PLOG(ERROR) << "Failed to create " << install_dir;
                 return INSTALL_ERROR_GENERIC;
@@ -905,7 +922,7 @@
             LOG(ERROR) << "cannot install GSIs to external media if verity uses check_at_most_once";
             return INSTALL_ERROR_GENERIC;
         }
-    } else if (install_dir != kDefaultDsuImageFolder) {
+    } else if (!android::base::StartsWith(install_dir, kDefaultDsuImageFolder)) {
         LOG(ERROR) << "cannot install DSU to " << install_dir;
         return INSTALL_ERROR_GENERIC;
     }
diff --git a/gsi_service.h b/gsi_service.h
index 3c4c278..0dce269 100644
--- a/gsi_service.h
+++ b/gsi_service.h
@@ -86,11 +86,11 @@
     std::string GetActiveInstalledImageDir();
 
     static std::vector<std::string> GetInstalledDsuSlots();
+    GsiService();
 
   private:
     friend class ImageService;
 
-    GsiService();
     static int ValidateInstallParams(std::string& install_dir);
     int EnableGsi(bool one_shot, const std::string& dsu_slot);
     bool DisableGsiInstall();
diff --git a/gsi_tool.cpp b/gsi_tool.cpp
index 181452b..6aa0fb5 100644
--- a/gsi_tool.cpp
+++ b/gsi_tool.cpp
@@ -19,6 +19,7 @@
 #include <sysexits.h>
 #include <unistd.h>
 
+#include <algorithm>
 #include <chrono>
 #include <condition_variable>
 #include <functional>
@@ -70,14 +71,50 @@
         // clang-format on
 };
 
+// Commands not allowed for locked DSU
+static const std::vector<std::string> kEnforceNonLockedDsu = {
+        // clang-format off
+        "disable",
+        "enable",
+        "wipe",
+        // clang-format on
+};
+
 static std::string ErrorMessage(const android::binder::Status& status,
                                 int error_code = IGsiService::INSTALL_ERROR_GENERIC) {
     if (!status.isOk()) {
-        return status.exceptionMessage().string();
+        return status.exceptionMessage().c_str();
     }
     return "error code " + std::to_string(error_code);
 }
 
+static inline bool IsRoot() {
+    return getuid() == 0;
+}
+
+static int EnforceNonLockedDsu(sp<IGsiService> gsid) {
+    bool running;
+    auto status = gsid->isGsiRunning(&running);
+    if (!status.isOk()) {
+        std::cerr << "Could not get DSU running status: " << ErrorMessage(status) << std::endl;
+        return EX_SOFTWARE;
+    }
+    if (!running) {
+        return 0;
+    }
+    std::string dsuSlot = {};
+    status = gsid->getActiveDsuSlot(&dsuSlot);
+    if (!status.isOk()) {
+        std::cerr << "Could not get the active DSU slot: " << ErrorMessage(status) << std::endl;
+        return EX_SOFTWARE;
+    }
+    if (android::base::EndsWith(dsuSlot, ".lock") && !IsRoot()) {
+        std::cerr << "Must be root to access a locked DSU" << std::endl;
+        return EX_NOPERM;
+    }
+    return 0;
+}
+
 class ProgressBar {
   public:
     explicit ProgressBar(sp<IGsiService> gsid) : gsid_(gsid) {}
@@ -210,7 +247,7 @@
     bool reboot = true;
     std::string installDir = "";
     std::string partition = kDefaultPartition;
-    if (getuid() != 0) {
+    if (!IsRoot()) {
         std::cerr << "must be root to install a GSI" << std::endl;
         return EX_NOPERM;
     }
@@ -376,7 +413,7 @@
         }
     }
 
-    if (getuid() != 0) {
+    if (!IsRoot()) {
         std::cerr << "must be root to install a DSU" << std::endl;
         return EX_NOPERM;
     }
@@ -496,7 +533,7 @@
     bool running;
     auto status = gsid->isGsiRunning(&running);
     if (!status.isOk()) {
-        std::cerr << "error: " << status.exceptionMessage().string() << std::endl;
+        std::cerr << "error: " << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     }
     if (running) {
@@ -507,7 +544,7 @@
     bool installed;
     status = gsid->isGsiInstalled(&installed);
     if (!status.isOk()) {
-        std::cerr << "error: " << status.exceptionMessage().string() << std::endl;
+        std::cerr << "error: " << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     }
     if (!installed) {
@@ -532,7 +569,7 @@
     bool running;
     auto status = gsid->isGsiRunning(&running);
     if (!status.isOk()) {
-        std::cerr << "error: " << status.exceptionMessage().string() << std::endl;
+        std::cerr << "error: " << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     } else if (running) {
         std::cout << "running" << std::endl;
@@ -540,7 +577,7 @@
     bool installed;
     status = gsid->isGsiInstalled(&installed);
     if (!status.isOk()) {
-        std::cerr << "error: " << status.exceptionMessage().string() << std::endl;
+        std::cerr << "error: " << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     } else if (installed) {
         std::cout << "installed" << std::endl;
@@ -548,21 +585,21 @@
     bool enabled;
     status = gsid->isGsiEnabled(&enabled);
     if (!status.isOk()) {
-        std::cerr << status.exceptionMessage().string() << std::endl;
+        std::cerr << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     } else if (running || installed) {
         std::cout << (enabled ? "enabled" : "disabled") << std::endl;
     } else {
         std::cout << "normal" << std::endl;
     }
-    if (getuid() != 0) {
+    if (!IsRoot()) {
         return 0;
     }
 
     std::vector<std::string> dsu_slots;
     status = gsid->getInstalledDsuSlots(&dsu_slots);
     if (!status.isOk()) {
-        std::cerr << status.exceptionMessage().string() << std::endl;
+        std::cerr << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     }
     int n = 0;
@@ -571,13 +608,18 @@
         sp<IImageService> image_service = nullptr;
         status = gsid->openImageService("dsu/" + dsu_slot + "/", &image_service);
         if (!status.isOk()) {
-            std::cerr << "error: " << status.exceptionMessage().string() << std::endl;
+            if (running) {
+                // openImageService through binder (gsid) could fail if running,
+                // because we can't stat the "outside" userdata.
+                continue;
+            }
+            std::cerr << "error: " << status.exceptionMessage().c_str() << std::endl;
             return EX_SOFTWARE;
         }
         std::vector<std::string> images;
         status = image_service->getAllBackingImages(&images);
         if (!status.isOk()) {
-            std::cerr << "error: " << status.exceptionMessage().string() << std::endl;
+            std::cerr << "error: " << status.exceptionMessage().c_str() << std::endl;
             return EX_SOFTWARE;
         }
         for (auto&& image : images) {
@@ -603,7 +645,7 @@
     bool cancelled = false;
     auto status = gsid->cancelGsiInstall(&cancelled);
     if (!status.isOk()) {
-        std::cerr << status.exceptionMessage().string() << std::endl;
+        std::cerr << status.exceptionMessage().c_str() << std::endl;
         return EX_SOFTWARE;
     }
     if (!cancelled) {
@@ -671,7 +713,6 @@
         std::cerr << "Unrecognized arguments to disable." << std::endl;
         return EX_USAGE;
     }
-
     bool installing = false;
     gsid->isGsiInstallInProgress(&installing);
     if (installing) {
@@ -729,12 +770,19 @@
 
     std::string command = argv[1];
 
+    int rc;
+    const auto& vec = kEnforceNonLockedDsu;
+    if (std::find(vec.begin(), vec.end(), command) != vec.end() &&
+        (rc = EnforceNonLockedDsu(service)) != 0) {
+        return rc;
+    }
+
     auto iter = kCommandMap.find(command);
     if (iter == kCommandMap.end()) {
         std::cerr << "Unrecognized command: " << command << std::endl;
         return usage(argc, argv);
     }
 
-    int rc = iter->second(service, argc - 1, argv + 1);
+    rc = iter->second(service, argc - 1, argv + 1);
     return rc;
 }
diff --git a/include/libgsi/libgsi.h b/include/libgsi/libgsi.h
index 5318341..969ea28 100644
--- a/include/libgsi/libgsi.h
+++ b/include/libgsi/libgsi.h
@@ -76,6 +76,8 @@
 // install_dir "/data/gsi/dsu/dsu2" has a slot name "dsu2"
 std::string GetDsuSlot(const std::string& install_dir);
 
+static constexpr char kDsuSlotProp[] = "ro.gsid.dsu_slot";
+
 static constexpr char kGsiBootedProp[] = "ro.gsid.image_running";
 
 static constexpr char kGsiInstalledProp[] = "gsid.image_installed";
diff --git a/libgsid.cpp b/libgsid.cpp
index 4689dcd..fc75c90 100644
--- a/libgsid.cpp
+++ b/libgsid.cpp
@@ -27,7 +27,7 @@
 sp<IGsiService> GetGsiService() {
     auto sm = android::defaultServiceManager();
     auto name = android::String16(kGsiServiceName);
-    android::sp<android::IBinder> res = sm->waitForService(name);
+    static android::sp<android::IBinder> res = sm->waitForService(name);
     if (res) {
         return android::interface_cast<IGsiService>(res);
     }
diff --git a/partition_installer.cpp b/partition_installer.cpp
index ebd3b64..ebfb329 100644
--- a/partition_installer.cpp
+++ b/partition_installer.cpp
@@ -256,6 +256,9 @@
 }
 
 int PartitionInstaller::GetPartitionFd() {
+    if (!system_device_) {
+        return -1;
+    }
     return system_device_->fd();
 }
 
diff --git a/tests/Android.bp b/tests/Android.bp
index 3035c10..e7e662c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -97,3 +97,20 @@
         "general-tests",
     ],
 }
+
+java_test_host {
+    name: "DsuGsiToolTest",
+    srcs: [
+        "DsuGsiToolTest.java",
+    ],
+    static_libs: [
+        "DsuTestBase",
+    ],
+    libs: [
+        "tradefed",
+    ],
+    test_config: "dsu_gsi_tool_test.xml",
+    test_suites: [
+        "general-tests",
+    ],
+}
diff --git a/tests/DSUEndtoEndTest.java b/tests/DSUEndtoEndTest.java
index 79799bf..8179d59 100644
--- a/tests/DSUEndtoEndTest.java
+++ b/tests/DSUEndtoEndTest.java
@@ -21,16 +21,20 @@
 import com.android.tradefed.config.Option;
 import com.android.tradefed.config.Option.Importance;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.SparseImageUtil;
+import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.ZipUtil2;
 
 import org.apache.commons.compress.archivers.zip.ZipFile;
+import org.junit.Before;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -40,7 +44,6 @@
 public class DSUEndtoEndTest extends DsuTestBase {
     private static final long kDefaultUserdataSize = 4L * 1024 * 1024 * 1024;
     private static final String LPUNPACK_PATH = "bin/lpunpack";
-    private static final String SIMG2IMG_PATH = "bin/simg2img";
 
     // Example: atest -v DSUEndtoEndTest -- --test-arg \
     // com.android.tradefed.testtype.HostTest:set-option:system_image_path:/full/path/to/system.img
@@ -51,56 +54,83 @@
             importance=Importance.ALWAYS)
     private String mSystemImagePath;
 
-    private File mUnsparseSystemImage;
+    private File mTempDir;
+
+    private File getTempPath(String relativePath) throws IOException {
+        if (mTempDir == null) {
+            mTempDir = FileUtil.createTempDir("DSUEndtoEndTest");
+        }
+        return new File(mTempDir, relativePath);
+    }
+
+    /** Extract system.img from build info to a temproary file. */
+    private File extractSystemImageFromBuildInfo(IBuildInfo buildInfo)
+            throws IOException, InterruptedException {
+        File imgZip = ((IDeviceBuildInfo) buildInfo).getDeviceImageFile();
+        Assert.assertNotNull(
+                "Failed to fetch system image. See system_image_path parameter", imgZip);
+
+        File superImg = getTempPath("super.img");
+        try (ZipFile zip = new ZipFile(imgZip)) {
+            File systemImg = getTempPath("system.img");
+            if (ZipUtil2.extractFileFromZip(zip, "system.img", systemImg)) {
+                return systemImg;
+            }
+            Assert.assertTrue(
+                    "No system.img or super.img in img zip.",
+                    ZipUtil2.extractFileFromZip(zip, "super.img", superImg));
+        }
+
+        if (SparseImageUtil.isSparse(superImg)) {
+            File unsparseSuperImage = getTempPath("super.raw");
+            SparseImageUtil.unsparse(superImg, unsparseSuperImage);
+            superImg = unsparseSuperImage;
+        }
+
+        File otaTools = buildInfo.getFile("otatools.zip");
+        Assert.assertNotNull("No otatools.zip in BuildInfo.", otaTools);
+        File otaToolsDir = getTempPath("otatools");
+        ZipUtil2.extractZip(otaTools, otaToolsDir);
+
+        String lpunpackPath = new File(otaToolsDir, LPUNPACK_PATH).getAbsolutePath();
+        File outputDir = getTempPath("lpunpack_output");
+        outputDir.mkdirs();
+        String[] cmd = {
+            lpunpackPath, "-p", "system_a", superImg.getAbsolutePath(), outputDir.getAbsolutePath()
+        };
+        Process p = Runtime.getRuntime().exec(cmd);
+        p.waitFor();
+        if (p.exitValue() != 0) {
+            String stderr = StreamUtil.getStringFromStream(p.getErrorStream());
+            Assert.fail(String.format("lpunpack returned %d. (%s)", p.exitValue(), stderr));
+        }
+        return new File(outputDir, "system_a.img");
+    }
+
+    @Before
+    public void setUp() {
+        mTempDir = null;
+    }
 
     @After
-    public void teardown() throws Exception {
-        if (mUnsparseSystemImage != null) {
-            mUnsparseSystemImage.delete();
-        }
+    public void tearDown() {
+        FileUtil.recursiveDelete(mTempDir);
     }
 
     @Test
     public void testDSU() throws Exception {
-        String simg2imgPath = "simg2img";
-        if (mSystemImagePath == null) {
-            IBuildInfo buildInfo = getBuild();
-            File imgs = ((IDeviceBuildInfo) buildInfo).getDeviceImageFile();
-            Assert.assertNotEquals("Failed to fetch system image. See system_image_path parameter", null, imgs);
-            File otaTools = buildInfo.getFile("otatools.zip");
-            File tempdir = ZipUtil2.extractZipToTemp(otaTools, "otatools");
-            File system = ZipUtil2.extractFileFromZip(new ZipFile(imgs), "system.img");
-            if (system == null) {
-                File superImg = ZipUtil2.extractFileFromZip(new ZipFile(imgs), "super.img");
-                String lpunpackPath = new File(tempdir, LPUNPACK_PATH).getAbsolutePath();
-                String outputDir = superImg.getParentFile().getAbsolutePath();
-                String[] cmd = {lpunpackPath, "-p", "system_a", superImg.getAbsolutePath(), outputDir};
-                Process p = Runtime.getRuntime().exec(cmd);
-                p.waitFor();
-                if (p.exitValue() == 0) {
-                    mSystemImagePath = new File(outputDir, "system_a.img").getAbsolutePath();
-                } else {
-                    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
-                    int len;
-                    byte[] buf = new byte[1024];
-                    while ((len = p.getErrorStream().read(buf)) != -1) {
-                          stderr.write(buf, 0, len);
-                    }
-                    Assert.assertEquals("non-zero exit value (" + stderr.toString("UTF-8") + ")", 0, p.exitValue());
-                }
-            } else {
-                mSystemImagePath = system.getAbsolutePath();
-            }
-            simg2imgPath = new File(tempdir, SIMG2IMG_PATH).getAbsolutePath();
+        File systemImage;
+        if (mSystemImagePath != null) {
+            systemImage = new File(mSystemImagePath);
+        } else {
+            systemImage = extractSystemImageFromBuildInfo(getBuild());
         }
-        File gsi = new File(mSystemImagePath);
-        Assert.assertTrue("not a valid file", gsi.isFile());
-        String[] cmd = {simg2imgPath, mSystemImagePath, mSystemImagePath + ".raw"};
-        Process p = Runtime.getRuntime().exec(cmd);
-        p.waitFor();
-        if (p.exitValue() == 0) {
-            mUnsparseSystemImage = new File(mSystemImagePath + ".raw");
-            gsi = mUnsparseSystemImage;
+        Assert.assertTrue("not a valid file", systemImage.isFile());
+
+        if (SparseImageUtil.isSparse(systemImage)) {
+            File unsparseSystemImage = getTempPath("system.raw");
+            SparseImageUtil.unsparse(systemImage, unsparseSystemImage);
+            systemImage = unsparseSystemImage;
         }
 
         boolean wasRoot = getDevice().isAdbRoot();
@@ -117,8 +147,8 @@
                                 "gsi_tool install --userdata-size %d"
                                         + " --gsi-size %d"
                                         + " && sleep 10000000",
-                                getDsuUserdataSize(kDefaultUserdataSize), gsi.length()),
-                        gsi,
+                                getDsuUserdataSize(kDefaultUserdataSize), systemImage.length()),
+                        systemImage,
                         null,
                         10,
                         TimeUnit.MINUTES,
diff --git a/tests/DsuGsiIntegrationTest.java b/tests/DsuGsiIntegrationTest.java
index 78d001c..bb0014a 100644
--- a/tests/DsuGsiIntegrationTest.java
+++ b/tests/DsuGsiIntegrationTest.java
@@ -122,6 +122,7 @@
                 getDevice().executeShellV2Command("gsi_tool wipe");
                 if (isDsuRunning()) {
                     getDevice().reboot();
+                    getDevice().disableAdbRoot();
                 }
             } catch (DeviceNotAvailableException e) {
                 CLog.w("Failed to clean up DSU installation on device: %s", e);
@@ -140,6 +141,7 @@
             CLog.i("Wipe existing DSU installation");
             assertShellCommand("gsi_tool wipe");
             getDevice().reboot();
+            getDevice().disableAdbRoot();
             assertDsuNotRunning();
         }
 
@@ -160,6 +162,7 @@
 
         CLog.i("Test 'gsi_tool enable -s' and 'gsi_tool enable'");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuNotRunning();
 
         final long freeSpaceAfterInstall = getDevice().getPartitionFreeSpace("/data") << 10;
@@ -173,12 +176,14 @@
 
         assertShellCommand("gsi_tool enable");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuRunning();
 
         CLog.i("Set up 'adb remount' for testing (requires reboot)");
         assertAdbRoot();
         assertShellCommand("remount");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuRunning();
         assertAdbRoot();
         assertShellCommand("remount");
@@ -190,12 +195,14 @@
         CLog.i("DSU and original system have separate remount overlays");
         assertShellCommand("gsi_tool disable");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuNotRunning();
         assertDevicePathNotExist(REMOUNT_TEST_PATH);
 
         CLog.i("Test that 'adb remount' is consistent after reboot");
         assertShellCommand("gsi_tool enable");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuRunning();
         assertDevicePathExist(REMOUNT_TEST_FILE);
         assertEquals(
@@ -208,12 +215,14 @@
         assertAdbRoot();
         assertShellCommand("enable-verity");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuRunning();
         assertDevicePathNotExist(REMOUNT_TEST_PATH);
 
         CLog.i("Test 'gsi_tool wipe'");
         assertShellCommand("gsi_tool wipe");
         getDevice().reboot();
+        getDevice().disableAdbRoot();
         assertDsuNotRunning();
 
         final double dampeningCoefficient = 0.9;
diff --git a/tests/DsuGsiToolTest.java b/tests/DsuGsiToolTest.java
new file mode 100644
index 0000000..c6e621a
--- /dev/null
+++ b/tests/DsuGsiToolTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 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.tests.dsu;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.util.CommandStatus;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class DsuGsiToolTest extends DsuTestBase {
+    private static final long DSU_MAX_WAIT_SEC = 10 * 60;
+
+    private String getDsuInstallCommand(String slotName) {
+        return String.format("am start-activity"
+                + " -n com.android.dynsystem/com.android.dynsystem.VerificationActivity"
+                + " -a android.os.image.action.START_INSTALL"
+                + " --el KEY_USERDATA_SIZE 2147483648"
+                + " --ez KEY_ENABLE_WHEN_COMPLETED true"
+                + " --es KEY_DSU_SLOT %s", slotName);
+    }
+
+    @Before
+    public void setUp() throws DeviceNotAvailableException {
+        if (isDsuRunning()) {
+            assertAdbRoot();
+            CLog.i("Wipe existing DSU installation");
+            assertShellCommand("gsi_tool wipe");
+            getDevice().reboot();
+            assertDsuNotRunning();
+        }
+        getDevice().disableAdbRoot();
+    }
+
+    @After
+    public void tearDown() throws DeviceNotAvailableException {
+        if (isDsuRunning()) {
+            getDevice().reboot();
+        }
+        getDevice().executeShellCommand("gsi_tool wipe");
+    }
+
+    @Test
+    public void testNonLockedDsu() throws DeviceNotAvailableException {
+        final String slotName = "foo";
+        assertShellCommand(getDsuInstallCommand(slotName));
+        CLog.i("Wait for DSU installation complete and reboot");
+        assertTrue(
+                "Timed out waiting for DSU installation complete",
+                getDevice().waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000));
+        CLog.i("DSU installation is complete and device is disconnected");
+
+        getDevice().waitForDeviceAvailable();
+        assertDsuRunning();
+        CLog.i("Successfully booted with DSU");
+
+        // These commands should run without any error
+        assertShellCommand("gsi_tool enable");
+        assertShellCommand("gsi_tool disable");
+        assertShellCommand("gsi_tool wipe");
+    }
+
+    @Test
+    public void testLockedDsu() throws DeviceNotAvailableException {
+        final String slotName = "foo.lock";
+        assertShellCommand(getDsuInstallCommand(slotName));
+        CLog.i("Wait for DSU installation complete and reboot");
+        assertTrue(
+                "Timed out waiting for DSU installation complete",
+                getDevice().waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000));
+        CLog.i("DSU installation is complete and device is disconnected");
+
+        getDevice().waitForDeviceAvailable();
+        assertDsuRunning();
+        CLog.i("Successfully booted with DSU");
+
+        // These commands should fail on a locked DSU
+        var result = getDevice().executeShellV2Command("gsi_tool enable");
+        assertFalse(result.getStatus() == CommandStatus.SUCCESS);
+        result = getDevice().executeShellV2Command("gsi_tool disable");
+        assertFalse(result.getStatus() == CommandStatus.SUCCESS);
+        result = getDevice().executeShellV2Command("gsi_tool wipe");
+        assertFalse(result.getStatus() == CommandStatus.SUCCESS);
+    }
+}
diff --git a/tests/boot_tests.cpp b/tests/boot_tests.cpp
index 9d5907c..e8f0a24 100644
--- a/tests/boot_tests.cpp
+++ b/tests/boot_tests.cpp
@@ -41,7 +41,15 @@
     return hw_type == "automotive";
 }
 
+bool ShouldRequireMetadata() {
+    int api_level = android::base::GetIntProperty("ro.product.first_api_level", -1);
+    return api_level >= __ANDROID_API_R__;
+}
+
 TEST(MetadataPartition, FirstStageMount) {
+    if (!ShouldRequireMetadata()) {
+        GTEST_SKIP();
+    }
     Fstab fstab;
     if (ReadFstabFromDt(&fstab)) {
         auto entry = GetEntryForMountPoint(&fstab, "/metadata");
@@ -59,6 +67,9 @@
 }
 
 TEST(MetadataPartition, MinimumSize) {
+    if (!ShouldRequireMetadata()) {
+        GTEST_SKIP();
+    }
     Fstab fstab;
     ASSERT_TRUE(ReadDefaultFstab(&fstab));
 
diff --git a/tests/dsu_gsi_tool_test.xml b/tests/dsu_gsi_tool_test.xml
new file mode 100644
index 0000000..d893902
--- /dev/null
+++ b/tests/dsu_gsi_tool_test.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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="Runs DsuGsiToolTest">
+    <option name="test-suite-tag" value="dsu-gsi-tool-test" />
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="com.android.tests.dsu.DsuGsiToolTest" />
+    </test>
+</configuration>