Merge "Migrate Test Targets to New Android Ownership Model" into main
diff --git a/create/avd_spec.py b/create/avd_spec.py
index f5da7fe..8b2f984 100644
--- a/create/avd_spec.py
+++ b/create/avd_spec.py
@@ -139,6 +139,7 @@
         self._ota_build_info = {}
         self._host_package_build_info = {}
         self._bootloader_build_info = {}
+        self._android_efi_loader_build_info = {}
         self._hw_property = None
         self._hw_customize = False
         self._remote_host = None
@@ -672,6 +673,9 @@
             constants.BUILD_ID: args.bootloader_build_id,
             constants.BUILD_BRANCH: args.bootloader_branch,
             constants.BUILD_TARGET: args.bootloader_build_target}
+        self._android_efi_loader_build_info = {
+            constants.BUILD_ID: args.android_efi_loader_build_id,
+            constants.BUILD_ARTIFACT: args.android_efi_loader_artifact}
         self._host_package_build_info = {
             constants.BUILD_ID: args.host_package_build_id,
             constants.BUILD_BRANCH: args.host_package_branch,
@@ -963,6 +967,11 @@
         return self._bootloader_build_info
 
     @property
+    def android_efi_loader_build_info(self):
+        """Return android efi loader build info."""
+        return self._android_efi_loader_build_info
+
+    @property
     def flavor(self):
         """Return flavor."""
         return self._flavor
diff --git a/create/create_args.py b/create/create_args.py
index cba574d..ce2e942 100644
--- a/create/create_args.py
+++ b/create/create_args.py
@@ -19,6 +19,7 @@
 import argparse
 import logging
 import os
+import posixpath as remote_path
 
 from acloud import errors
 from acloud.create import create_common
@@ -173,6 +174,18 @@
         help="'cuttlefish only' Bootloader build target.",
         required=False)
     parser.add_argument(
+        "--android-efi-loader-build-id",
+        type=str,
+        dest="android_efi_loader_build_id",
+        help="'cuttlefish only' Android EFI loader build id, e.g. P2804227",
+        required=False)
+    parser.add_argument(
+        "--android-efi-loader-artifact",
+        type=str,
+        dest="android_efi_loader_artifact",
+        help="'cuttlefish only' Android EFI loader artifact name, e.g. gbl_aarch64.efi",
+        required=False)
+    parser.add_argument(
         "--kernel-build-id",
         type=str,
         dest="kernel_build_id",
@@ -882,9 +895,14 @@
         raise errors.UnsupportedCreateArgs(
             "--host-ssh-private-key-path is only supported for remote host.")
 
-    if args.remote_image_dir and args.remote_host is None:
-        raise errors.UnsupportedCreateArgs(
-            "--remote-image-dir is only supported for remote host.")
+    if args.remote_image_dir:
+        if args.remote_host is None:
+            raise errors.UnsupportedCreateArgs(
+                "--remote-image-dir is only supported for remote host.")
+        if remote_path.basename(
+                remote_path.normpath(args.remote_image_dir)) in ("..", "."):
+            raise errors.UnsupportedCreateArgs(
+                "--remote-image-dir must not include the working directory.")
 
 
 def _VerifyGoldfishArgs(args):
diff --git a/create/remote_image_local_instance.py b/create/remote_image_local_instance.py
index a49b63d..e8feaf3 100644
--- a/create/remote_image_local_instance.py
+++ b/create/remote_image_local_instance.py
@@ -122,6 +122,7 @@
         avd_spec.kernel_build_info,
         avd_spec.boot_build_info,
         avd_spec.bootloader_build_info,
+        avd_spec.android_efi_loader_build_info,
         avd_spec.ota_build_info,
         avd_spec.host_package_build_info)
 
diff --git a/internal/lib/android_build_client.py b/internal/lib/android_build_client.py
index 05e08bc..4960bd0 100644
--- a/internal/lib/android_build_client.py
+++ b/internal/lib/android_build_client.py
@@ -66,9 +66,12 @@
     LATEST = "latest"
     # FETCH_CVD variables.
     FETCHER_NAME = "fetch_cvd"
-    FETCHER_BRANCH = "aosp-master"
-    FETCHER_BUILD_TARGET = "aosp_cf_x86_64_phone-userdebug"
-    FETCHER_ARM_VERSION_BUILD_TARGET = "aosp_cf_arm64_phone-userdebug"
+    FETCHER_BUILD_TARGET = "aosp_cf_x86_64_phone-trunk_staging-userdebug"
+    FETCHER_BUILD_TARGET_ARM = "aosp_cf_arm64_only_phone-trunk_staging-userdebug"
+    # TODO(b/297085994): cvd fetch is migrating from AOSP to github artifacts, so
+    # temporary returning hardcoded values instead of LKGB
+    FETCHER_BUILD_ID = 11559438
+    FETCHER_BUILD_ID_ARM = 11559085
     MAX_RETRY = 3
     RETRY_SLEEP_SECS = 3
 
@@ -128,9 +131,9 @@
             is_arm_version: is ARM version fetch_cvd.
         """
         if fetch_cvd_version == constants.LKGB:
-            fetch_cvd_version = self.GetFetcherVersion()
+            fetch_cvd_version = self.GetFetcherVersion(is_arm_version)
         fetch_cvd_build_target = (
-            self.FETCHER_ARM_VERSION_BUILD_TARGET if is_arm_version
+            self.FETCHER_BUILD_TARGET_ARM if is_arm_version
             else self.FETCHER_BUILD_TARGET)
         try:
             utils.RetryExceptionType(
@@ -162,11 +165,12 @@
         os.chmod(local_dest, fetch_cvd_stat.st_mode | stat.S_IEXEC)
 
     @staticmethod
-    def ProcessBuild(build_info):
+    def ProcessBuild(build_info, ignore_artifact=False):
         """Create a Cuttlefish fetch_cvd build string.
 
         Args:
             build_info: The dictionary that contains build information.
+            ignore_artifact: Avoid adding artifact part to fetch_cvd build string
 
         Returns:
             A string, used in the fetch_cvd cmd or None if all args are None.
@@ -174,17 +178,22 @@
         build_id = build_info.get(constants.BUILD_ID)
         build_target = build_info.get(constants.BUILD_TARGET)
         branch = build_info.get(constants.BUILD_BRANCH)
-        if not build_target:
-            return build_id or branch
+        artifact = build_info.get(constants.BUILD_ARTIFACT)
 
-        if build_target and not branch:
-            branch = _DEFAULT_BRANCH
-        return (build_id or branch) + "/" + build_target
+        result = build_id or branch
+        if build_target is not None:
+            result = result or _DEFAULT_BRANCH
+            result += "/" + build_target
+
+        if not ignore_artifact and artifact:
+            result += "{" + artifact + "}"
+
+        return result
 
     def GetFetchBuildArgs(self, default_build_info, system_build_info,
                           kernel_build_info, boot_build_info,
-                          bootloader_build_info, ota_build_info,
-                          host_package_build_info):
+                          bootloader_build_info, android_efi_loader_build_info,
+                          ota_build_info, host_package_build_info):
         """Get args from build information for fetch_cvd.
 
         Each build_info is a dictionary that contains 3 items, for example,
@@ -203,6 +212,7 @@
                              constants.BUILD_ARTIFACT which is mapped to the
                              boot image name.
             bootloader_build_info: The build that provides the bootloader.
+            android_efi_loader_build_info: The build that provides the Android EFI loader.
             ota_build_info: The build that provides the OTA tools.
             host_package_build_info: The build that provides the host package.
 
@@ -220,10 +230,13 @@
         bootloader_build = self.ProcessBuild(bootloader_build_info)
         if bootloader_build:
             fetch_cvd_args.append(f"-bootloader_build={bootloader_build}")
+        android_efi_loader_build = self.ProcessBuild(android_efi_loader_build_info)
+        if android_efi_loader_build:
+            fetch_cvd_args.append(f"-android_efi_loader_build {android_efi_loader_build}")
         kernel_build = self.GetKernelBuild(kernel_build_info)
         if kernel_build:
             fetch_cvd_args.append(f"-kernel_build={kernel_build}")
-        boot_build = self.ProcessBuild(boot_build_info)
+        boot_build = self.ProcessBuild(boot_build_info, ignore_artifact=True)
         if boot_build:
             fetch_cvd_args.append(f"-boot_build={boot_build}")
             boot_artifact = boot_build_info.get(constants.BUILD_ARTIFACT)
@@ -238,13 +251,16 @@
 
         return fetch_cvd_args
 
-    def GetFetcherVersion(self):
+    def GetFetcherVersion(self, is_arm_version=False):
         """Get fetch_cvd build id from LKGB.
 
         Returns:
             The build id of fetch_cvd.
         """
-        return self.GetLKGB(self.FETCHER_BUILD_TARGET, self.FETCHER_BRANCH)
+        # TODO(b/297085994): currently returning hardcoded values
+        # For more information, please check the BUILD_ID constant definition
+        # comment section
+        return self.FETCHER_BUILD_ID_ARM if is_arm_version else self.FETCHER_BUILD_ID
 
     @staticmethod
     # pylint: disable=broad-except
diff --git a/internal/lib/android_build_client_test.py b/internal/lib/android_build_client_test.py
index 30e1df4..247a2f7 100644
--- a/internal/lib/android_build_client_test.py
+++ b/internal/lib/android_build_client_test.py
@@ -179,6 +179,10 @@
         ota_build = {constants.BUILD_ID: "4567",
                      constants.BUILD_BRANCH: "ota_branch",
                      constants.BUILD_TARGET: "ota_target"}
+        bootloader_build = {constants.BUILD_ID: "10111213",
+                            constants.BUILD_TARGET: "boot_crosvm_x86_64"}
+        android_efi_loader_build = {constants.BUILD_ID: "6789",
+                                    constants.BUILD_ARTIFACT: "gbl_x86_32.efi"}
         boot_build = {constants.BUILD_ID: "5678",
                       constants.BUILD_BRANCH: "boot_branch",
                       constants.BUILD_TARGET: "boot_target",
@@ -192,7 +196,7 @@
         self.assertEqual(
             expected_args,
             self.client.GetFetchBuildArgs(
-                default_build, {}, {}, {}, {}, {}, {}))
+                default_build, {}, {}, {}, {}, {}, {}, {}))
 
         # Test base image with system image.
         expected_args = ["-default_build=1234/base_target",
@@ -200,7 +204,7 @@
         self.assertEqual(
             expected_args,
             self.client.GetFetchBuildArgs(
-                default_build, system_build, {}, {}, {}, {}, {}))
+                default_build, system_build, {}, {}, {}, {}, {}, {}))
 
         # Test base image with kernel image.
         expected_args = ["-default_build=1234/base_target",
@@ -208,7 +212,7 @@
         self.assertEqual(
             expected_args,
             self.client.GetFetchBuildArgs(
-                default_build, {}, kernel_build, {}, {}, {}, {}))
+                default_build, {}, kernel_build, {}, {}, {}, {}, {}))
 
         # Test base image with boot image.
         expected_args = ["-default_build=1234/base_target",
@@ -217,7 +221,23 @@
         self.assertEqual(
             expected_args,
             self.client.GetFetchBuildArgs(
-                default_build, {}, {}, boot_build, {}, {}, {}))
+                default_build, {}, {}, boot_build, {}, {}, {}, {}))
+
+        # Test base image with bootloader.
+        expected_args = ["-default_build=1234/base_target",
+                         "-bootloader_build=10111213/boot_crosvm_x86_64"]
+        self.assertEqual(
+            expected_args,
+            self.client.GetFetchBuildArgs(
+                default_build, {}, {}, {}, bootloader_build, {}, {}, {}))
+
+        # Test base image with android efi.
+        expected_args = ["-default_build=1234/base_target",
+                         "-android_efi_loader_build 6789{gbl_x86_32.efi}"]
+        self.assertEqual(
+            expected_args,
+            self.client.GetFetchBuildArgs(
+                default_build, {}, {}, {}, {}, android_efi_loader_build, {}, {}))
 
         # Test base image with otatools.
         expected_args = ["-default_build=1234/base_target",
@@ -225,7 +245,7 @@
         self.assertEqual(
             expected_args,
             self.client.GetFetchBuildArgs(
-                default_build, {}, {}, {}, {}, ota_build, {}))
+                default_build, {}, {}, {}, {}, {}, ota_build, {}))
 
         # Test base image with host_package.
         expected_args = ["-default_build=1234/base_target",
@@ -233,7 +253,7 @@
         self.assertEqual(
             expected_args,
             self.client.GetFetchBuildArgs(
-                default_build, {}, {}, {}, {}, {}, host_package_build))
+                default_build, {}, {}, {}, {}, {}, {}, host_package_build))
 
     def testGetFetchCertArg(self):
         """Test GetFetchCertArg."""
diff --git a/internal/lib/cvd_compute_client_multi_stage.py b/internal/lib/cvd_compute_client_multi_stage.py
index 6e72667..c6ba1b9 100644
--- a/internal/lib/cvd_compute_client_multi_stage.py
+++ b/internal/lib/cvd_compute_client_multi_stage.py
@@ -325,7 +325,7 @@
     @utils.TimeExecute(function_description="Downloading build on instance")
     def FetchBuild(self, default_build_info, system_build_info,
                    kernel_build_info, boot_build_info, bootloader_build_info,
-                   ota_build_info, host_package_build_info):
+                   android_efi_loader_build_info, ota_build_info, host_package_build_info):
         """Execute fetch_cvd on the remote instance to get Cuttlefish runtime files.
 
         Args:
@@ -334,6 +334,7 @@
             kernel_build_info: The build that provides the kernel.
             boot_build_info: The build that provides the boot image.
             bootloader_build_info: The build that provides the bootloader.
+            android_efi_loader_build_info: The build that provides the Android EFI app.
             ota_build_info: The build that provides the OTA tools.
             host_package_build_info: The build that provides the host package.
 
@@ -344,8 +345,8 @@
         fetch_cvd_args = ["-credential_source=gce"]
         fetch_cvd_build_args = self._build_api.GetFetchBuildArgs(
             default_build_info, system_build_info, kernel_build_info,
-            boot_build_info, bootloader_build_info, ota_build_info,
-            host_package_build_info)
+            boot_build_info, bootloader_build_info, android_efi_loader_build_info,
+            ota_build_info, host_package_build_info)
         fetch_cvd_args.extend(fetch_cvd_build_args)
 
         self._ssh.Run("./fetch_cvd " + " ".join(fetch_cvd_args),
diff --git a/internal/lib/cvd_runtime_config_test.py b/internal/lib/cvd_runtime_config_test.py
index ae77ef0..a702ce0 100644
--- a/internal/lib/cvd_runtime_config_test.py
+++ b/internal/lib/cvd_runtime_config_test.py
@@ -162,15 +162,15 @@
     def testGetIdFromInstanceDirStr(self):
         """Test GetIdFromInstanceDirStr."""
         fake_instance_dir = "/path-to-instance-dir"
-        self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir), None)
+        self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir, None), None)
 
         fake_instance_dir = "/fake-path/local-instance-1/"
-        self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir), "1")
+        self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir, None), "1")
 
         fake_home_path = "/home/fake_user/"
         self.Patch(os.path, 'expanduser', return_value=fake_home_path)
         fake_instance_dir = "/home/fake_user/local-instance/"
-        self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir), "1")
+        self.assertEqual(cf_cfg._GetIdFromInstanceDirStr(fake_instance_dir, None), "1")
 
 
 if __name__ == "__main__":
diff --git a/internal/lib/cvd_utils.py b/internal/lib/cvd_utils.py
index bb6804c..9d5a9b7 100644
--- a/internal/lib/cvd_utils.py
+++ b/internal/lib/cvd_utils.py
@@ -17,12 +17,16 @@
 import collections
 import fnmatch
 import glob
+import json
 import logging
 import os
 import posixpath as remote_path
+import random
 import re
+import shlex
 import subprocess
 import tempfile
+import time
 import zipfile
 
 from acloud import errors
@@ -75,6 +79,11 @@
     _REMOTE_EXTRA_IMAGE_DIR, _INITRAMFS_IMAGE_NAME)
 _REMOTE_SUPER_IMAGE_PATH = remote_path.join(
     _REMOTE_EXTRA_IMAGE_DIR, _SUPER_IMAGE_NAME)
+# The symbolic link to --remote-image-dir. It's in the base directory.
+_IMAGE_DIR_LINK_NAME = "image_dir_link"
+# The text file contains the number of references to --remote-image-dir.
+# Th path is --remote-image-dir + EXT.
+_REF_CNT_FILE_EXT = ".lock"
 
 # Remote host instance name
 _REMOTE_HOST_INSTANCE_NAME_FORMAT = (
@@ -314,7 +323,8 @@
         search_path: A path to an image file or an image directory.
 
     Returns:
-        A list of strings, the launch_cvd arguments including the remote paths.
+        A list of string pairs. Each pair consists of a launch_cvd option and a
+        remote path.
 
     Raises:
         errors.GetLocalImageError if search_path does not contain kernel
@@ -332,22 +342,22 @@
             remote_image_dir, _REMOTE_INITRAMFS_IMAGE_PATH)
         ssh_obj.ScpPushFile(kernel_image_path, remote_kernel_image_path)
         ssh_obj.ScpPushFile(initramfs_image_path, remote_initramfs_image_path)
-        return ["-kernel_path", remote_kernel_image_path,
-                "-initramfs_path", remote_initramfs_image_path]
+        return [("-kernel_path", remote_kernel_image_path),
+                ("-initramfs_path", remote_initramfs_image_path)]
 
     boot_image_path, vendor_boot_image_path = FindBootImages(search_path)
     if boot_image_path:
         remote_boot_image_path = remote_path.join(
             remote_image_dir, _REMOTE_BOOT_IMAGE_PATH)
         ssh_obj.ScpPushFile(boot_image_path, remote_boot_image_path)
-        launch_cvd_args = ["-boot_image", remote_boot_image_path]
+        launch_cvd_args = [("-boot_image", remote_boot_image_path)]
         if vendor_boot_image_path:
             remote_vendor_boot_image_path = remote_path.join(
                 remote_image_dir, _REMOTE_VENDOR_BOOT_IMAGE_PATH)
             ssh_obj.ScpPushFile(vendor_boot_image_path,
                                 remote_vendor_boot_image_path)
-            launch_cvd_args.extend(["-vendor_boot_image",
-                                    remote_vendor_boot_image_path])
+            launch_cvd_args.append(("-vendor_boot_image",
+                                    remote_vendor_boot_image_path))
         return launch_cvd_args
 
     raise errors.GetLocalImageError(
@@ -442,12 +452,12 @@
         vbmeta_image_path: The path to the vbmeta image.
 
     Returns:
-        A list of strings, the launch_cvd arguments including the remote paths.
+        A pair of strings, the launch_cvd option and the remote path.
     """
     remote_vbmeta_image_path = remote_path.join(remote_image_dir,
                                                 _REMOTE_VBMETA_IMAGE_PATH)
     ssh_obj.ScpPushFile(vbmeta_image_path, remote_vbmeta_image_path)
-    return ["-vbmeta_image", remote_vbmeta_image_path]
+    return "-vbmeta_image", remote_vbmeta_image_path
 
 
 def AreTargetFilesRequired(avd_spec):
@@ -470,7 +480,8 @@
                           avd_spec requires building a super image.
 
     Returns:
-        A list of strings, the launch_cvd arguments including the remote paths.
+        A list of string pairs. Each pair consists of a launch_cvd option and a
+        remote path.
 
     Raises:
         errors.GetLocalImageError if any specified image path does not exist.
@@ -498,13 +509,13 @@
         with tempfile.TemporaryDirectory() as super_image_dir:
             _MixSuperImage(os.path.join(super_image_dir, _SUPER_IMAGE_NAME),
                            avd_spec, target_files_dir, ota)
-            extra_img_args += _UploadSuperImage(ssh_obj, remote_image_dir,
-                                                super_image_dir)
+            extra_img_args.append(_UploadSuperImage(ssh_obj, remote_image_dir,
+                                                    super_image_dir))
 
             vbmeta_image_path = os.path.join(super_image_dir, "vbmeta.img")
             ota.MakeDisabledVbmetaImage(vbmeta_image_path)
-            extra_img_args += _UploadVbmetaImage(ssh_obj, remote_image_dir,
-                                                 vbmeta_image_path)
+            extra_img_args.append(_UploadVbmetaImage(ssh_obj, remote_image_dir,
+                                                     vbmeta_image_path))
 
     return extra_img_args
 
@@ -519,7 +530,7 @@
         super_image_dir: The path to the directory containing the super image.
 
     Returns:
-        A list of strings, the launch_cvd arguments including the remote paths.
+        A pair of strings, the launch_cvd option and the remote path.
     """
     remote_super_image_path = remote_path.join(remote_image_dir,
                                                _REMOTE_SUPER_IMAGE_PATH)
@@ -528,8 +539,7 @@
            f"{ssh_obj.GetBaseCmd(constants.SSH_BIN)} -- "
            f"tar -xf - --lzop -S -C {remote_super_image_dir}")
     ssh.ShellCmdWithRetry(cmd)
-    launch_cvd_args = ["-super_image", remote_super_image_path]
-    return launch_cvd_args
+    return "-super_image", remote_super_image_path
 
 
 def CleanUpRemoteCvd(ssh_obj, remote_dir, raise_error):
@@ -544,7 +554,9 @@
     Raises:
         subprocess.CalledProcessError if any command fails.
     """
-    # TODO(b/293966645): Find stop_cvd in --remote-image-dir.
+    # FIXME: Use the images and launch_cvd in --remote-image-dir when
+    # cuttlefish can reliably share images.
+    _DeleteRemoteImageDirLink(ssh_obj, remote_dir)
     home = remote_path.join("$HOME", remote_dir)
     stop_cvd_path = remote_path.join(remote_dir, "bin", "stop_cvd")
     stop_cvd_cmd = f"'HOME={home} {stop_cvd_path}'"
@@ -557,7 +569,7 @@
             logger.debug(
                 "Failed to stop_cvd (possibly no running device): %s", e)
 
-    # This command deletes all files except hidden files under HOME.
+    # This command deletes all files except hidden files under remote_dir.
     # It does not raise an error if no files can be deleted.
     ssh_obj.Run(f"'rm -rf {remote_path.join(remote_dir, '*')}'")
 
@@ -611,6 +623,161 @@
     return None
 
 
+def PrepareRemoteImageDirLink(ssh_obj, remote_dir, remote_image_dir):
+    """Create a link to a directory containing images and tools.
+
+    Args:
+        ssh_obj: An Ssh object.
+        remote_dir: The directory in which the link is created.
+        remote_image_dir: The directory that is linked to.
+    """
+    remote_link = remote_path.join(remote_dir, _IMAGE_DIR_LINK_NAME)
+
+    # If remote_image_dir is relative to HOME, compute the relative path based
+    # on remote_dir.
+    ln_cmd = ("ln -s " +
+              ("" if remote_path.isabs(remote_image_dir) else "-r ") +
+              f"{remote_image_dir} {remote_link}")
+
+    remote_ref_cnt = remote_path.normpath(remote_image_dir) + _REF_CNT_FILE_EXT
+    ref_cnt_cmd = (f"expr $(test -s {remote_ref_cnt} && "
+                   f"cat {remote_ref_cnt} || echo 0) + 1 > {remote_ref_cnt}")
+
+    # `flock` creates the file automatically.
+    # This command should create its parent directory before `flock`.
+    ssh_obj.Run(shlex.quote(
+        f"mkdir -p {remote_image_dir} && flock {remote_ref_cnt} -c " +
+        shlex.quote(
+            f"mkdir -p {remote_dir} {remote_image_dir} && "
+            f"{ln_cmd} && {ref_cnt_cmd}")))
+
+
+def _DeleteRemoteImageDirLink(ssh_obj, remote_dir):
+    """Delete the directories containing images and tools.
+
+    Args:
+        ssh_obj: An Ssh object.
+        remote_dir: The directory containing the link to the image directory.
+    """
+    remote_link = remote_path.join(remote_dir, _IMAGE_DIR_LINK_NAME)
+    # This command returns an absolute path if the link exists; otherwise
+    # an empty string. It raises an exception only if connection error.
+    remote_image_dir = ssh_obj.Run(
+        shlex.quote(f"readlink -n -e {remote_link} || true"))
+    if not remote_image_dir:
+        return
+
+    remote_ref_cnt = (remote_path.normpath(remote_image_dir) +
+                      _REF_CNT_FILE_EXT)
+    # `expr` returns 1 if the result is 0.
+    ref_cnt_cmd = (f"expr $(test -s {remote_ref_cnt} && "
+                   f"cat {remote_ref_cnt} || echo 1) - 1 > "
+                   f"{remote_ref_cnt}")
+
+    # `flock` creates the file automatically.
+    # This command should create its parent directory before `flock`.
+    ssh_obj.Run(shlex.quote(
+        f"mkdir -p {remote_image_dir} && flock {remote_ref_cnt} -c " +
+        shlex.quote(
+            f"rm -f {remote_link} && "
+            f"{ref_cnt_cmd} || "
+            f"rm -rf {remote_image_dir} {remote_ref_cnt}")))
+
+
+def LoadRemoteImageArgs(ssh_obj, remote_timestamp_path, remote_args_path,
+                        deadline):
+    """Load launch_cvd arguments from a remote path.
+
+    Acloud processes using the same --remote-image-dir synchronizes on
+    remote_timestamp_path and remote_args_path in the directory. This function
+    implements the synchronization in 3 steps:
+
+    1. This function checks whether remote_timestamp_path is empty. If it is,
+    this acloud process becomes the uploader. This function writes the upload
+    deadline to the file and returns None. The caller should upload files to
+    the --remote-image-dir and then call SaveRemoteImageArgs. The upload
+    deadline written to the file represents when this acloud process should
+    complete uploading.
+
+    2. If remote_timestamp_path is not empty, this function reads the upload
+    deadline from it. It then waits until remote_args_path contains the
+    arguments in a valid format, or the upload deadline passes.
+
+    3. If this function loads arguments from remote_args_path successfully,
+    it returns the arguments. Otherwise, the uploader misses the deadline. The
+    --remote-image-dir is not usable. This function raises an error. It does
+    not attempt to reset the --remote-image-dir.
+
+    Args:
+        ssh_obj: An Ssh object.
+        remote_timestamp_path: The remote path containing the time when the
+                               uploader will complete.
+        remote_args_path: The remote path where the arguments are loaded.
+        deadline: The deadline written to remote_timestamp_path if this process
+                  becomes the uploader.
+
+    Returns:
+        A list of string pairs, the arguments generated by UploadExtraImages.
+        None if the directory has not been initialized.
+
+    Raises:
+        errors.CreateError if timeout.
+    """
+    timeout = int(deadline - time.time())
+    if timeout <= 0:
+        raise errors.CreateError("Timed out before loading remote image args.")
+
+    timestamp_cmd = (f"test -s {remote_timestamp_path} && "
+                     f"cat {remote_timestamp_path} || "
+                     f"expr $(date +%s) + {timeout} > {remote_timestamp_path}")
+    upload_deadline = ssh_obj.Run(shlex.quote(
+        f"flock {remote_timestamp_path} -c " +
+        shlex.quote(timestamp_cmd))).strip()
+    if not upload_deadline:
+        return None
+
+    # Wait until remote_args_path is not empty or upload_deadline <= now.
+    wait_cmd = (f"test -s {remote_args_path} -o "
+                f"{upload_deadline} -le $(date +%s) || echo wait...")
+    timeout = deadline - time.time()
+    utils.PollAndWait(
+        lambda : ssh_obj.Run(shlex.quote(
+            f"flock {remote_args_path} -c " + shlex.quote(wait_cmd))),
+        expected_return="",
+        timeout_exception=errors.CreateError(
+            f"{remote_args_path} is not ready within {timeout} secs"),
+        timeout_secs=timeout,
+        sleep_interval_secs=10 + random.uniform(0, 5))
+
+    args_str = ssh_obj.Run(shlex.quote(
+        f"flock {remote_args_path} -c " +
+        shlex.quote(f"cat {remote_args_path}")))
+    if not args_str:
+        raise errors.CreateError(
+            f"The uploader did not meet the deadline {upload_deadline}. "
+            f"{remote_args_path} is unusable.")
+    try:
+        return json.loads(args_str)
+    except json.JSONDecodeError as e:
+        raise errors.CreateError(f"Cannot load {remote_args_path}: {e}")
+
+
+def SaveRemoteImageArgs(ssh_obj, remote_args_path, launch_cvd_args):
+    """Save launch_cvd arguments to a remote path.
+
+    Args:
+        ssh_obj: An Ssh object.
+        remote_args_path: The remote path where the arguments are saved.
+        launch_cvd_args: A list of string pairs, the arguments generated by
+                         UploadExtraImages.
+    """
+    # args_str is interpreted three times by SSH, remote shell, and flock.
+    args_str = shlex.quote(json.dumps(launch_cvd_args))
+    ssh_obj.Run(shlex.quote(
+        f"flock {remote_args_path} -c " +
+        shlex.quote(f"echo {args_str} > {remote_args_path}")))
+
+
 def GetConfigFromRemoteAndroidInfo(ssh_obj, remote_image_dir):
     """Get config from android-info.txt on a remote host or a GCE instance.
 
@@ -712,16 +879,11 @@
     Returns:
         A string, the launch_cvd command.
     """
-    # launch_cvd requires ANDROID_HOST_OUT to be absolute.
-    cmd = ([f"{constants.ENV_ANDROID_HOST_OUT}="
-            f"$(readlink -n -m {avd_spec.remote_image_dir})",
-            f"{constants.ENV_ANDROID_PRODUCT_OUT}="
-            f"${constants.ENV_ANDROID_HOST_OUT}"]
-           if avd_spec.remote_image_dir else [])
-    cmd.extend(["HOME=" + remote_path.join("$HOME", remote_dir),
-                remote_path.join(avd_spec.remote_image_dir or remote_dir,
-                                 "bin", "launch_cvd"),
-                "-daemon"])
+    # FIXME: Use the images and launch_cvd in avd_spec.remote_image_dir when
+    # cuttlefish can reliably share images.
+    cmd = ["HOME=" + remote_path.join("$HOME", remote_dir),
+           remote_path.join(remote_dir, "bin", "launch_cvd"),
+           "-daemon"]
     cmd.extend(extra_args)
     cmd.extend(_GetLaunchCvdArgs(avd_spec, config))
     return " ".join(cmd)
@@ -951,6 +1113,10 @@
         {"bootloader_" + key: val
          for key, val in avd_spec.bootloader_build_info.items() if val}
     )
+    build_info_dict.update(
+        {"android_efi_loader_" + key: val
+         for key, val in avd_spec.android_efi_loader_build_info.items() if val}
+    )
     return build_info_dict
 
 
diff --git a/internal/lib/cvd_utils_test.py b/internal/lib/cvd_utils_test.py
index 34294f8..6455a77 100644
--- a/internal/lib/cvd_utils_test.py
+++ b/internal/lib/cvd_utils_test.py
@@ -159,7 +159,7 @@
                                       local_vendor_image=None)
             args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
                                                None)
-            self.assertEqual(["-boot_image", "dir/acloud_image/boot.img"],
+            self.assertEqual([("-boot_image", "dir/acloud_image/boot.img")],
                              args)
             mock_ssh.Run.assert_called_once_with("mkdir -p dir/acloud_image")
             mock_ssh.ScpPushFile.assert_called_once_with(
@@ -170,8 +170,8 @@
             args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
                                                None)
             self.assertEqual(
-                ["-boot_image", "dir/acloud_image/boot.img",
-                 "-vendor_boot_image", "dir/acloud_image/vendor_boot.img"],
+                [("-boot_image", "dir/acloud_image/boot.img"),
+                 ("-vendor_boot_image", "dir/acloud_image/vendor_boot.img")],
                 args)
             mock_ssh.Run.assert_called_once()
             self.assertEqual(2, mock_ssh.ScpPushFile.call_count)
@@ -198,8 +198,8 @@
             args = cvd_utils.UploadExtraImages(mock_ssh, "dir", mock_avd_spec,
                                                None)
             self.assertEqual(
-                ["-kernel_path", "dir/acloud_image/kernel",
-                 "-initramfs_path", "dir/acloud_image/initramfs.img"],
+                [("-kernel_path", "dir/acloud_image/kernel"),
+                 ("-initramfs_path", "dir/acloud_image/initramfs.img")],
                 args)
             mock_ssh.Run.assert_called_once()
             self.assertEqual(2, mock_ssh.ScpPushFile.call_count)
@@ -232,8 +232,8 @@
                                                target_files_dir)
 
         self.assertEqual(
-            ["-super_image", "dir/acloud_image/super.img",
-             "-vbmeta_image", "dir/acloud_image/vbmeta.img"],
+            [("-super_image", "dir/acloud_image/super.img"),
+             ("-vbmeta_image", "dir/acloud_image/vbmeta.img")],
             args)
         mock_find_ota_tools.assert_called_once_with([])
         mock_ssh.Run.assert_called_once_with("mkdir -p dir/acloud_image")
@@ -261,18 +261,36 @@
     def testCleanUpRemoteCvd(self):
         """Test CleanUpRemoteCvd."""
         mock_ssh = mock.Mock()
+        mock_ssh.Run.side_effect = ["", "", ""]
         cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=True)
-        mock_ssh.Run.assert_any_call("'HOME=$HOME/dir dir/bin/stop_cvd'")
-        mock_ssh.Run.assert_any_call("'rm -rf dir/*'")
+        mock_ssh.Run.assert_has_calls([
+            mock.call("'readlink -n -e dir/image_dir_link || true'"),
+            mock.call("'HOME=$HOME/dir dir/bin/stop_cvd'"),
+            mock.call("'rm -rf dir/*'")])
+
+        mock_ssh.reset_mock()
+        mock_ssh.Run.side_effect = ["img_dir", "", "", ""]
+        cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=True)
+        mock_ssh.Run.assert_has_calls([
+            mock.call("'readlink -n -e dir/image_dir_link || true'"),
+            mock.call("'mkdir -p img_dir && flock img_dir.lock -c '\"'\"'"
+                      "rm -f dir/image_dir_link && "
+                      "expr $(test -s img_dir.lock && "
+                      "cat img_dir.lock || echo 1) - 1 > img_dir.lock || "
+                      "rm -rf img_dir img_dir.lock'\"'\"''"),
+            mock.call("'HOME=$HOME/dir dir/bin/stop_cvd'"),
+            mock.call("'rm -rf dir/*'")])
 
         mock_ssh.reset_mock()
         mock_ssh.Run.side_effect = [
+            "",
             subprocess.CalledProcessError(cmd="should raise", returncode=1)]
         with self.assertRaises(subprocess.CalledProcessError):
             cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=True)
 
         mock_ssh.reset_mock()
         mock_ssh.Run.side_effect = [
+            "",
             subprocess.CalledProcessError(cmd="should ignore", returncode=1),
             None]
         cvd_utils.CleanUpRemoteCvd(mock_ssh, "dir", raise_error=False)
@@ -309,6 +327,158 @@
             "host-goldfish-192.0.2.1-5554-123456-sdk_x86_64-sdk")
         self.assertIsNone(result)
 
+    # pylint: disable=protected-access
+    def testRemoteImageDirLink(self):
+        """Test PrepareRemoteImageDirLink and _DeleteRemoteImageDirLink."""
+        self.assertEqual(os.path, cvd_utils.remote_path)
+        with tempfile.TemporaryDirectory(prefix="cvd_utils") as temp_dir:
+            env = os.environ.copy()
+            env["HOME"] = temp_dir
+            # Execute the commands locally.
+            mock_ssh = mock.Mock()
+            mock_ssh.Run.side_effect = lambda cmd: subprocess.check_output(
+                "sh -c " + cmd, shell=True, cwd=temp_dir, env=env
+            ).decode("utf-8")
+            # Relative paths under temp_dir.
+            base_dir_name_1 = "acloud_cf_1"
+            base_dir_name_2 = "acloud_cf_2"
+            image_dir_name = "test/img"
+            rel_ref_cnt_path = "test/img.lock"
+            # Absolute paths.
+            image_dir = os.path.join(temp_dir, image_dir_name)
+            ref_cnt_path = os.path.join(temp_dir, rel_ref_cnt_path)
+            link_path_1 = os.path.join(temp_dir, base_dir_name_1,
+                                       "image_dir_link")
+            link_path_2 = os.path.join(temp_dir, base_dir_name_2,
+                                       "image_dir_link")
+            # Delete non-existing directories.
+            cvd_utils._DeleteRemoteImageDirLink(mock_ssh, base_dir_name_1)
+            mock_ssh.Run.assert_called_with(
+                f"'readlink -n -e {base_dir_name_1}/image_dir_link || true'")
+            self.assertFalse(
+                os.path.exists(os.path.join(temp_dir, base_dir_name_1)))
+            self.assertFalse(os.path.exists(image_dir))
+            self.assertFalse(os.path.exists(ref_cnt_path))
+            # Prepare the first base dir.
+            cvd_utils.PrepareRemoteImageDirLink(mock_ssh, base_dir_name_1,
+                                                image_dir_name)
+            mock_ssh.Run.assert_called_with(
+                f"'mkdir -p {image_dir_name} && flock {rel_ref_cnt_path} -c "
+                f"'\"'\"'mkdir -p {base_dir_name_1} {image_dir_name} && "
+                f"ln -s -r {image_dir_name} "
+                f"{base_dir_name_1}/image_dir_link && "
+                f"expr $(test -s {rel_ref_cnt_path} && "
+                f"cat {rel_ref_cnt_path} || echo 0) + 1 > "
+                f"{rel_ref_cnt_path}'\"'\"''")
+            self.assertTrue(os.path.islink(link_path_1))
+            self.assertEqual("../test/img", os.readlink(link_path_1))
+            self.assertTrue(os.path.isfile(ref_cnt_path))
+            with open(ref_cnt_path, "r", encoding="utf-8") as ref_cnt_file:
+                self.assertEqual("1\n", ref_cnt_file.read())
+            # Prepare the second base dir.
+            cvd_utils.PrepareRemoteImageDirLink(mock_ssh, base_dir_name_2,
+                                                image_dir_name)
+            self.assertTrue(os.path.islink(link_path_2))
+            self.assertEqual("../test/img", os.readlink(link_path_2))
+            self.assertTrue(os.path.isfile(ref_cnt_path))
+            with open(ref_cnt_path, "r", encoding="utf-8") as ref_cnt_file:
+                self.assertEqual("2\n", ref_cnt_file.read())
+            # Delete the first base dir.
+            cvd_utils._DeleteRemoteImageDirLink(mock_ssh, base_dir_name_1)
+            self.assertFalse(os.path.lexists(link_path_1))
+            self.assertTrue(os.path.isfile(ref_cnt_path))
+            with open(ref_cnt_path, "r", encoding="utf-8") as ref_cnt_file:
+                self.assertEqual("1\n", ref_cnt_file.read())
+            # Delete the second base dir.
+            cvd_utils._DeleteRemoteImageDirLink(mock_ssh, base_dir_name_2)
+            self.assertFalse(os.path.lexists(link_path_2))
+            self.assertFalse(os.path.exists(image_dir))
+            self.assertFalse(os.path.exists(ref_cnt_path))
+
+    @mock.patch("acloud.internal.lib.cvd_utils.utils.PollAndWait")
+    @mock.patch("acloud.internal.lib.cvd_utils.utils.time.time",
+                return_value=90.0)
+    def testLoadRemoteImageArgs(self, _mock_time, mock_poll_and_wait):
+        """Test LoadRemoteImageArgs."""
+        deadline = 99.9
+        self.assertEqual(os.path, cvd_utils.remote_path)
+
+        with tempfile.TemporaryDirectory(prefix="cvd_utils") as temp_dir:
+            env = os.environ.copy()
+            env["HOME"] = temp_dir
+            # Execute the commands locally.
+            mock_ssh = mock.Mock()
+            mock_ssh.Run.side_effect = lambda cmd: subprocess.check_output(
+                "sh -c " + cmd, shell=True, cwd=temp_dir, env=env, text=True)
+            mock_poll_and_wait.side_effect = lambda func, **kwargs: func()
+
+            timestamp_path = os.path.join(temp_dir, "timestamp.txt")
+            args_path = os.path.join(temp_dir, "args.txt")
+
+            # Test with an uninitialized directory.
+            args = cvd_utils.LoadRemoteImageArgs(
+                mock_ssh, timestamp_path, args_path, deadline)
+
+            self.assertIsNone(args)
+            mock_ssh.Run.assert_called_once()
+            with open(timestamp_path, "r", encoding="utf-8") as timestamp_file:
+                timestamp = timestamp_file.read().strip()
+                self.assertRegex(timestamp, r"\d+",
+                                 f"Invalid timestamp: {timestamp}")
+            self.assertFalse(os.path.exists(args_path))
+
+            # Test with an initialized directory and the uploader times out.
+            mock_ssh.Run.reset_mock()
+
+            with self.assertRaises(errors.CreateError):
+                cvd_utils.LoadRemoteImageArgs(
+                    mock_ssh, timestamp_path, args_path, deadline)
+
+            mock_ssh.Run.assert_has_calls([
+                mock.call(f"'flock {timestamp_path} -c '\"'\"'"
+                          f"test -s {timestamp_path} && "
+                          f"cat {timestamp_path} || "
+                          f"expr $(date +%s) + 9 > {timestamp_path}'\"'\"''"),
+                mock.call(f"'flock {args_path} -c '\"'\"'"
+                          f"test -s {args_path} -o "
+                          f"{timestamp} -le $(date +%s) || "
+                          "echo wait...'\"'\"''"),
+                mock.call(f"'flock {args_path} -c '\"'\"'"
+                          f"cat {args_path}'\"'\"''")
+            ])
+            with open(timestamp_path, "r", encoding="utf-8") as timestamp_file:
+                self.assertEqual(timestamp_file.read().strip(), timestamp)
+            self.assertEqual(os.path.getsize(args_path), 0)
+
+            # Test with an initialized directory.
+            mock_ssh.Run.reset_mock()
+            self.CreateFile(args_path, b'[["arg", "1"]]')
+
+            args = cvd_utils.LoadRemoteImageArgs(
+                mock_ssh, timestamp_path, args_path, deadline)
+
+            self.assertEqual(args, [["arg", "1"]])
+            self.assertEqual(mock_ssh.Run.call_count, 3)
+
+    def testSaveRemoteImageArgs(self):
+        """Test SaveRemoteImageArgs."""
+        with tempfile.TemporaryDirectory(prefix="cvd_utils") as temp_dir:
+            env = os.environ.copy()
+            env["HOME"] = temp_dir
+            mock_ssh = mock.Mock()
+            mock_ssh.Run.side_effect = lambda cmd: subprocess.check_call(
+                "sh -c " + cmd, shell=True, cwd=temp_dir, env=env, text=True)
+            args_path = os.path.join(temp_dir, "args.txt")
+
+            cvd_utils.SaveRemoteImageArgs(mock_ssh, args_path, [("arg", "1")])
+
+            mock_ssh.Run.assert_called_with(
+                f"'flock {args_path} -c '\"'\"'"
+                f"""echo '"'"'"'"'"'"'"'"'[["arg", "1"]]'"'"'"'"'"'"'"'"' > """
+                f"{args_path}'\"'\"''")
+            with open(args_path, "r", encoding="utf-8") as args_file:
+                self.assertEqual(args_file.read().strip(), '[["arg", "1"]]')
+
     def testGetConfigFromRemoteAndroidInfo(self):
         """Test GetConfigFromRemoteAndroidInfo."""
         mock_ssh = mock.Mock()
@@ -334,7 +504,6 @@
             cfg=mock_cfg,
             hw_customize=False,
             hw_property=hw_property,
-            remote_image_dir=None,
             connect_webrtc=False,
             connect_vnc=False,
             openwrt=False,
@@ -364,7 +533,6 @@
             cfg=mock_cfg,
             hw_customize=True,
             hw_property=hw_property,
-            remote_image_dir="img_dir",
             connect_webrtc=True,
             webrtc_device_id="pet-name",
             connect_vnc=True,
@@ -373,10 +541,7 @@
             base_instance_num=3,
             launch_args="--setupwizard_mode=REQUIRED")
         expected_cmd = (
-            "ANDROID_HOST_OUT=$(readlink -n -m img_dir) "
-            "ANDROID_PRODUCT_OUT=$ANDROID_HOST_OUT "
-            "HOME=$HOME/dir "
-            "img_dir/bin/launch_cvd -daemon --extra args "
+            "HOME=$HOME/dir dir/bin/launch_cvd -daemon --extra args "
             "-data_policy=create_if_missing -blank_data_image_mb=20480 "
             "-config=phone -x_res=1080 -y_res=1920 -dpi=240 "
             "-data_policy=always_create -blank_data_image_mb=10240 "
@@ -568,7 +733,8 @@
             remote_image=remote_image,
             kernel_build_info={"build_target": "kernel"},
             system_build_info={},
-            bootloader_build_info={})
+            bootloader_build_info={},
+            android_efi_loader_build_info = {})
         self.assertEqual(remote_image,
                          cvd_utils.GetRemoteBuildInfoDict(mock_avd_spec))
 
@@ -584,6 +750,10 @@
             "branch": "aosp_u-boot-mainline",
             "build_id": "400000",
             "build_target": "u-boot_crosvm_x86_64"}
+        android_efi_loader_build_info = {
+            "build_id": "500000",
+            "artifact": "gbl_aarch64.efi"
+        }
         all_build_info = {
             "kernel_branch": "aosp_kernel-common-android12-5.10",
             "kernel_build_id": "200000",
@@ -593,14 +763,18 @@
             "system_build_target": "aosp_x86_64-userdebug",
             "bootloader_branch": "aosp_u-boot-mainline",
             "bootloader_build_id": "400000",
-            "bootloader_build_target": "u-boot_crosvm_x86_64"}
+            "bootloader_build_target": "u-boot_crosvm_x86_64",
+            "android_efi_loader_build_id": "500000",
+            "android_efi_loader_artifact": "gbl_aarch64.efi"
+        }
         all_build_info.update(remote_image)
         mock_avd_spec = mock.Mock(
             spec=[],
             remote_image=remote_image,
             kernel_build_info=kernel_build_info,
             system_build_info=system_build_info,
-            bootloader_build_info=bootloader_build_info)
+            bootloader_build_info=bootloader_build_info,
+            android_efi_loader_build_info=android_efi_loader_build_info)
         self.assertEqual(all_build_info,
                          cvd_utils.GetRemoteBuildInfoDict(mock_avd_spec))
 
diff --git a/internal/lib/goldfish_compute_client.py b/internal/lib/goldfish_compute_client.py
index c251912..9ae7922 100644
--- a/internal/lib/goldfish_compute_client.py
+++ b/internal/lib/goldfish_compute_client.py
@@ -165,8 +165,7 @@
                        avd_spec=None,
                        extra_scopes=None,
                        tags=None,
-                       launch_args=None,
-                       disable_external_ip=False):
+                       launch_args=None):
         """Create a goldfish instance given a stable host image and a build id.
 
         Args:
@@ -191,8 +190,6 @@
             tags: A list of tags to associate with the instance. e.g.
                  ["http-server", "https-server"]
             launch_args: String of args for launch command.
-            disable_external_ip: Boolean, true if instance external ip should be
-                                 disabled.
         """
         self._VerifyZoneByQuota()
         self._CheckMachineSize()
@@ -261,5 +258,4 @@
             zone=self._zone,
             gpu=gpu,
             tags=tags,
-            extra_scopes=extra_scopes,
-            disable_external_ip=disable_external_ip)
+            extra_scopes=extra_scopes)
diff --git a/internal/lib/goldfish_compute_client_test.py b/internal/lib/goldfish_compute_client_test.py
index e5b3981..02f649a 100644
--- a/internal/lib/goldfish_compute_client_test.py
+++ b/internal/lib/goldfish_compute_client_test.py
@@ -199,8 +199,7 @@
             zone=self.ZONE,
             gpu=self.GPU,
             tags=self.TAGS,
-            extra_scopes=self.EXTRA_SCOPES,
-            disable_external_ip=False)
+            extra_scopes=self.EXTRA_SCOPES)
 
     @mock.patch("getpass.getuser", return_value="fake_user")
     def testCreateInstanceWithAvdSpec(self, _mock_user):
@@ -271,8 +270,7 @@
             zone=self.ZONE,
             gpu=self.GPU,
             tags=self.TAGS,
-            extra_scopes=self.EXTRA_SCOPES,
-            disable_external_ip=False)
+            extra_scopes=self.EXTRA_SCOPES)
 
 
 if __name__ == "__main__":
diff --git a/internal/lib/ssh.py b/internal/lib/ssh.py
index 53916b8..524a297 100755
--- a/internal/lib/ssh.py
+++ b/internal/lib/ssh.py
@@ -29,7 +29,7 @@
 _SSH_CMD = ("-i %(rsa_key_file)s -o LogLevel=ERROR -o ControlPath=none "
             "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no")
 _SSH_IDENTITY = "-l %(login_user)s %(ip_addr)s"
-_SSH_CMD_MAX_RETRY = 5
+SSH_CMD_DEFAULT_RETRY = 5
 _SSH_CMD_RETRY_SLEEP = 3
 _CONNECTION_TIMEOUT = 10
 _MAX_REPORTED_ERROR_LINES = 10
@@ -108,6 +108,9 @@
         show_output: Boolean, True to show command output in screen.
         hide_error_msg: Boolean, True to hide error message.
 
+    Returns:
+        A string, stdout and stderr.
+
     Raises:
         errors.DeviceConnectionError: Failed to connect to the GCE instance.
         subprocess.CalledProcessError: The process exited with an error on the instance.
@@ -145,6 +148,7 @@
         if constants.ERROR_MSG_WEBRTC_NOT_SUPPORT in stdout:
             raise errors.LaunchCVDFail(constants.ERROR_MSG_WEBRTC_NOT_SUPPORT)
         raise subprocess.CalledProcessError(process.returncode, cmd)
+    return stdout
 
 
 def _GetErrorMessage(stdout):
@@ -185,7 +189,7 @@
 
 
 def ShellCmdWithRetry(cmd, timeout=None, show_output=False,
-                      retry=_SSH_CMD_MAX_RETRY):
+                      retry=SSH_CMD_DEFAULT_RETRY):
     """Runs a shell command on remote device.
 
     If the network is unstable and causes SSH connect fail, it will retry. When
@@ -199,12 +203,15 @@
         show_output: Boolean, True to show command output in screen.
         retry: Integer, the retry times.
 
+    Returns:
+        A string, stdout and stderr.
+
     Raises:
         errors.DeviceConnectionError: For any non-zero return code of remote_cmd.
         errors.LaunchCVDFail: Happened on launch_cvd with specific pattern of error message.
         subprocess.CalledProcessError: The process exited with an error on the instance.
     """
-    utils.RetryExceptionType(
+    return utils.RetryExceptionType(
         exception_types=(errors.DeviceConnectionError,
                          errors.LaunchCVDFail,
                          subprocess.CalledProcessError),
@@ -257,7 +264,7 @@
                 extra_args_ssh_tunnel)
 
     def Run(self, target_command, timeout=None, show_output=False,
-            retry=_SSH_CMD_MAX_RETRY):
+            retry=SSH_CMD_DEFAULT_RETRY):
         """Run a shell command over SSH on a remote instance.
 
         Example:
@@ -273,11 +280,15 @@
             timeout: Integer, the maximum time to wait for the command to respond.
             show_output: Boolean, True to show command output in screen.
             retry: Integer, the retry times.
+
+        Returns:
+            A string, stdout and stderr.
         """
-        ShellCmdWithRetry(self.GetBaseCmd(constants.SSH_BIN) + " " + target_command,
-                          timeout,
-                          show_output,
-                          retry)
+        return ShellCmdWithRetry(
+            self.GetBaseCmd(constants.SSH_BIN) + " " + target_command,
+            timeout,
+            show_output,
+            retry)
 
     def GetBaseCmd(self, execute_bin):
         """Get a base command over SSH on a remote instance.
@@ -346,7 +357,7 @@
                 "Ssh isn't ready in the remote instance.") from e
 
     @utils.TimeExecute(function_description="Waiting for SSH server")
-    def WaitForSsh(self, timeout=None, max_retry=_SSH_CMD_MAX_RETRY):
+    def WaitForSsh(self, timeout=None, max_retry=SSH_CMD_DEFAULT_RETRY):
         """Wait until the remote instance is ready to accept commands over SSH.
 
         Args:
diff --git a/internal/lib/ssh_test.py b/internal/lib/ssh_test.py
index d35a608..72ba397 100644
--- a/internal/lib/ssh_test.py
+++ b/internal/lib/ssh_test.py
@@ -93,8 +93,9 @@
     def testSshRunCmd(self):
         """Test ssh run command."""
         self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
+        self.created_subprocess.communicate.return_value = ("stdout", "")
         ssh_object = ssh.Ssh(self.FAKE_IP, self.FAKE_SSH_USER, self.FAKE_SSH_PRIVATE_KEY_PATH)
-        ssh_object.Run("command")
+        self.assertEqual("stdout", ssh_object.Run("command"))
         expected_cmd = (
             "exec /usr/bin/ssh -i /fake/acloud_rea -o LogLevel=ERROR -o ControlPath=none "
             "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
@@ -109,11 +110,12 @@
     def testSshRunCmdwithExtraArgs(self):
         """test ssh rum command with extra command."""
         self.Patch(subprocess, "Popen", return_value=self.created_subprocess)
+        self.created_subprocess.communicate.return_value = ("stdout", "")
         ssh_object = ssh.Ssh(self.FAKE_IP,
                              self.FAKE_SSH_USER,
                              self.FAKE_SSH_PRIVATE_KEY_PATH,
                              self.FAKE_EXTRA_ARGS_SSH)
-        ssh_object.Run("command")
+        self.assertEqual("stdout", ssh_object.Run("command"))
         expected_cmd = (
             "exec /usr/bin/ssh -i /fake/acloud_rea -o LogLevel=ERROR -o ControlPath=none "
             "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
diff --git a/public/acloud_main.py b/public/acloud_main.py
index 308ee21..3e3ef62 100644
--- a/public/acloud_main.py
+++ b/public/acloud_main.py
@@ -435,8 +435,7 @@
             autoconnect=args.autoconnect,
             tags=args.tags,
             report_internal_ip=args.report_internal_ip,
-            boot_timeout_secs=args.boot_timeout_secs,
-            disable_external_ip=args.disable_external_ip)
+            boot_timeout_secs=args.boot_timeout_secs)
     elif args.which == delete_args.CMD_DELETE:
         reporter = delete.Run(args)
     elif args.which == list_args.CMD_LIST:
@@ -482,10 +481,11 @@
         EXIT_CODE = constants.EXIT_BY_ERROR
         EXCEPTION_STACKTRACE = traceback.format_exc()
         EXCEPTION_LOG = str(e)
-        raise
-    finally:
-        # Log Exit event here to calculate the consuming time.
-        if LOG_METRICS:
-            metrics.LogExitEvent(EXIT_CODE,
-                                 stacktrace=EXCEPTION_STACKTRACE,
-                                 logs=EXCEPTION_LOG)
+        sys.stderr.write("Exception: %s" % (EXCEPTION_STACKTRACE))
+
+    # Log Exit event here to calculate the consuming time.
+    if LOG_METRICS:
+        metrics.LogExitEvent(EXIT_CODE,
+                             stacktrace=EXCEPTION_STACKTRACE,
+                             logs=EXCEPTION_LOG)
+    sys.exit(EXIT_CODE)
diff --git a/public/actions/create_goldfish_action.py b/public/actions/create_goldfish_action.py
index aae9b8b..d461743 100644
--- a/public/actions/create_goldfish_action.py
+++ b/public/actions/create_goldfish_action.py
@@ -59,7 +59,6 @@
                  "/home/vsoc-01/log/adb.log",
                  "/var/log/daemon.log"]
 
-    #pylint: disable=too-many-locals
     def __init__(self,
                  cfg,
                  build_target,
@@ -73,8 +72,7 @@
                  avd_spec=None,
                  tags=None,
                  branch=None,
-                 emulator_branch=None,
-                 disable_external_ip=False):
+                 emulator_branch=None):
 
         """Initialize.
 
@@ -90,8 +88,6 @@
                   ["http-server", "https-server"]
             branch: String, branch of the emulator build target.
             emulator_branch: String, branch of the emulator.
-            disable_external_ip: Boolean, true if instance external ip should be
-                                 disabled.
         """
 
         self.credentials = auth.CreateCredentials(cfg)
@@ -107,7 +103,6 @@
         self._blank_data_disk_size_gb = cfg.extra_data_disk_size_gb
         self._extra_scopes = cfg.extra_scopes
         self._tags = tags
-        self._disable_external_ip = disable_external_ip
 
         # Configure clients
         self._build_client = android_build_client.AndroidBuildClient(
@@ -173,8 +168,7 @@
             avd_spec=self._avd_spec,
             tags=self._tags,
             extra_scopes=self._extra_scopes,
-            launch_args=self._cfg.launch_args,
-            disable_external_ip=self._disable_external_ip)
+            launch_args=self._cfg.launch_args)
 
         return instance
 
@@ -252,8 +246,7 @@
                   branch=None,
                   tags=None,
                   report_internal_ip=False,
-                  boot_timeout_secs=None,
-                  disable_external_ip=False):
+                  boot_timeout_secs=None):
     """Create one or multiple Goldfish devices.
 
     Args:
@@ -281,8 +274,6 @@
                             external ip.
         boot_timeout_secs: Integer, the maximum time in seconds used to
                            wait for the AVD to boot.
-        disable_external_ip: Boolean, true if instance external ip should be
-                                 disabled.
 
     Returns:
         A Report instance.
@@ -302,7 +293,6 @@
         report_internal_ip = avd_spec.report_internal_ip
         client_adb_port = avd_spec.client_adb_port
         boot_timeout_secs = avd_spec.boot_timeout_secs
-        disable_external_ip = avd_spec.disable_external_ip
 
     if not emulator_build_target:
         emulator_build_target = cfg.emulator_build_target
@@ -339,10 +329,10 @@
         "Creating a goldfish device in project %s, build_target: %s, "
         "build_id: %s, emulator_bid: %s, emulator_branch: %s, kernel_build_id: %s, "
         "kernel_branch: %s, kernel_build_target: %s, GPU: %s, num: %s, "
-        "serial_log_file: %s, autoconnect: %s, disable_external_ip: %s",
-        cfg.project, build_target, build_id, emulator_build_id,
-        emulator_branch, kernel_build_id, kernel_branch, kernel_build_target,
-        gpu, num, serial_log_file, autoconnect, disable_external_ip)
+        "serial_log_file: %s, "
+        "autoconnect: %s", cfg.project, build_target, build_id,
+        emulator_build_id, emulator_branch, kernel_build_id, kernel_branch,
+        kernel_build_target, gpu, num, serial_log_file, autoconnect)
 
     device_factory = GoldfishDeviceFactory(
         cfg, build_target, build_id,
@@ -353,8 +343,7 @@
         emulator_branch=emulator_branch,
         kernel_build_id=kernel_build_id,
         kernel_branch=kernel_branch,
-        kernel_build_target=kernel_build_target,
-        disable_external_ip=disable_external_ip)
+        kernel_build_target=kernel_build_target)
 
     return common_operations.CreateDevices("create_gf", cfg, device_factory,
                                            num, constants.TYPE_GF,
diff --git a/public/actions/create_goldfish_action_test.py b/public/actions/create_goldfish_action_test.py
index b5d7b51..b784be4 100644
--- a/public/actions/create_goldfish_action_test.py
+++ b/public/actions/create_goldfish_action_test.py
@@ -82,7 +82,6 @@
         self.avd_spec.gpu = self.GPU
         self.avd_spec.serial_log_file = None
         self.avd_spec.autoconnect = False
-        self.avd_spec.disable_external_ip = False
 
     def _CreateCfg(self):
         """A helper method that creates a mock configuration object."""
@@ -154,8 +153,7 @@
             avd_spec=none_avd_spec,
             extra_scopes=self.EXTRA_SCOPES,
             tags=None,
-            launch_args=self.LAUNCH_ARGS,
-            disable_external_ip=False)
+            launch_args=self.LAUNCH_ARGS)
 
         self.assertEqual(report.data, {
             "devices": [
@@ -213,8 +211,7 @@
             avd_spec=self.avd_spec,
             extra_scopes=self.EXTRA_SCOPES,
             tags=None,
-            launch_args=self.LAUNCH_ARGS,
-            disable_external_ip=False)
+            launch_args=self.LAUNCH_ARGS)
 
     def testCreateDevicesWithoutBuildId(self):
         """Test CreateDevices when emulator sysimage buildid is not provided."""
@@ -280,8 +277,7 @@
             avd_spec=none_avd_spec,
             extra_scopes=self.EXTRA_SCOPES,
             tags=None,
-            launch_args=self.LAUNCH_ARGS,
-            disable_external_ip=False)
+            launch_args=self.LAUNCH_ARGS)
 
         self.assertEqual(report.data, {
             "devices": [{
@@ -337,8 +333,7 @@
             avd_spec=self.avd_spec,
             extra_scopes=self.EXTRA_SCOPES,
             tags=None,
-            launch_args=self.LAUNCH_ARGS,
-            disable_external_ip=False)
+            launch_args=self.LAUNCH_ARGS)
 
     #pylint: disable=invalid-name
     def testCreateDevicesWithoutEmulatorBuildId(self):
@@ -397,8 +392,7 @@
             avd_spec=none_avd_spec,
             extra_scopes=self.EXTRA_SCOPES,
             tags=None,
-            launch_args=self.LAUNCH_ARGS,
-            disable_external_ip=False)
+            launch_args=self.LAUNCH_ARGS)
 
         self.assertEqual(report.data, {
             "devices": [{
@@ -454,8 +448,7 @@
             avd_spec=self.avd_spec,
             extra_scopes=self.EXTRA_SCOPES,
             tags=None,
-            launch_args=self.LAUNCH_ARGS,
-            disable_external_ip=False)
+            launch_args=self.LAUNCH_ARGS)
 
 
 if __name__ == "__main__":
diff --git a/public/actions/remote_host_cf_device_factory.py b/public/actions/remote_host_cf_device_factory.py
index 403e15d..134922d 100644
--- a/public/actions/remote_host_cf_device_factory.py
+++ b/public/actions/remote_host_cf_device_factory.py
@@ -41,6 +41,8 @@
 _ALL_FILES = "*"
 _HOME_FOLDER = os.path.expanduser("~")
 _TEMP_PREFIX = "acloud_remote_host"
+_IMAGE_TIMESTAMP_FILE_NAME = "acloud_image_timestamp.txt"
+_IMAGE_ARGS_FILE_NAME = "acloud_image_args.txt"
 
 
 class RemoteHostDeviceFactory(base_device_factory.BaseDeviceFactory):
@@ -90,16 +92,29 @@
             A string, representing instance name.
         """
         start_time = time.time()
+        self._compute_client.SetStage(constants.STAGE_SSH_CONNECT)
         instance = self._InitRemotehost()
         start_time = self._compute_client.RecordTime(
             constants.TIME_GCE, start_time)
 
-        process_artifacts_timestart = start_time
-        image_args = self._ProcessRemoteHostArtifacts()
-        start_time = self._compute_client.RecordTime(
-            constants.TIME_ARTIFACT, start_time)
+        deadline = start_time + (self._avd_spec.boot_timeout_secs or
+                                 constants.DEFAULT_CF_BOOT_TIMEOUT)
+        self._compute_client.SetStage(constants.STAGE_ARTIFACT)
+        try:
+            image_args = self._ProcessRemoteHostArtifacts(deadline)
+        except (errors.CreateError, errors.DriverError,
+                subprocess.CalledProcessError) as e:
+            logger.exception("Fail to prepare artifacts.")
+            self._all_failures[instance] = str(e)
+            # If an SSH error or timeout happens, report the name for the
+            # caller to clean up this instance.
+            return instance
+        finally:
+            start_time = self._compute_client.RecordTime(
+                constants.TIME_ARTIFACT, start_time)
 
-        error_msg = self._LaunchCvd(image_args, process_artifacts_timestart)
+        self._compute_client.SetStage(constants.STAGE_BOOT_UP)
+        error_msg = self._LaunchCvd(image_args, deadline)
         start_time = self._compute_client.RecordTime(
             constants.TIME_LAUNCH, start_time)
 
@@ -147,7 +162,6 @@
         Returns:
             A string, representing instance name.
         """
-        self._compute_client.SetStage(constants.STAGE_SSH_CONNECT)
         # Get product name from the img zip file name or TARGET_PRODUCT.
         image_name = os.path.basename(
             self._local_image_artifact) if self._local_image_artifact else ""
@@ -174,8 +188,48 @@
                                    raise_error=False)
         return instance
 
-    def _ProcessRemoteHostArtifacts(self):
-        """Process remote host artifacts.
+    def _ProcessRemoteHostArtifacts(self, deadline):
+        """Initialize or reuse the images on the remote host.
+
+        Args:
+            deadline: The timestamp when the timeout expires.
+
+        Returns:
+            A list of strings, the launch_cvd arguments.
+        """
+        remote_image_dir = self._avd_spec.remote_image_dir
+        reuse_remote_image_dir = False
+        if remote_image_dir:
+            remote_args_path = remote_path.join(remote_image_dir,
+                                                _IMAGE_ARGS_FILE_NAME)
+            cvd_utils.PrepareRemoteImageDirLink(
+                self._ssh, self._GetInstancePath(), remote_image_dir)
+            launch_cvd_args = cvd_utils.LoadRemoteImageArgs(
+                self._ssh,
+                remote_path.join(remote_image_dir, _IMAGE_TIMESTAMP_FILE_NAME),
+                remote_args_path, deadline)
+            if launch_cvd_args is not None:
+                logger.info("Reuse the images in %s", remote_image_dir)
+                reuse_remote_image_dir = True
+            logger.info("Create images in %s", remote_image_dir)
+
+        if not reuse_remote_image_dir:
+            launch_cvd_args = self._InitRemoteImageDir()
+
+        if remote_image_dir:
+            if not reuse_remote_image_dir:
+                cvd_utils.SaveRemoteImageArgs(self._ssh, remote_args_path,
+                                              launch_cvd_args)
+            # FIXME: Use the images in remote_image_dir when cuttlefish can
+            # reliably share images.
+            launch_cvd_args = self._ReplaceRemoteImageArgs(
+                launch_cvd_args, remote_image_dir, self._GetInstancePath())
+            self._CopyRemoteImageDir(remote_image_dir, self._GetInstancePath())
+
+        return [arg for arg_pair in launch_cvd_args for arg in arg_pair]
+
+    def _InitRemoteImageDir(self):
+        """Create remote host artifacts.
 
         - If images source is local, tool will upload images from local site to
           remote host.
@@ -184,10 +238,9 @@
           is no permission to fetch build rom on the remote host.
 
         Returns:
-            A list of strings, the launch_cvd arguments.
+            A list of string pairs, the launch_cvd arguments generated by
+            UploadExtraImages.
         """
-        # TODO(b/293966645): Check if --remote-image-dir is initialized.
-        self._compute_client.SetStage(constants.STAGE_ARTIFACT)
         self._ssh.Run(f"mkdir -p {self._GetArtifactPath()}")
 
         launch_cvd_args = []
@@ -288,6 +341,7 @@
             self._avd_spec.kernel_build_info,
             self._avd_spec.boot_build_info,
             self._avd_spec.bootloader_build_info,
+            self._avd_spec.android_efi_loader_build_info,
             self._avd_spec.ota_build_info,
             self._avd_spec.host_package_build_info)
 
@@ -314,6 +368,7 @@
             self._avd_spec.kernel_build_info,
             self._avd_spec.boot_build_info,
             self._avd_spec.bootloader_build_info,
+            self._avd_spec.android_efi_loader_build_info,
             self._avd_spec.ota_build_info,
             self._avd_spec.host_package_build_info)
 
@@ -373,6 +428,7 @@
             self._avd_spec.kernel_build_info,
             self._avd_spec.boot_build_info,
             self._avd_spec.bootloader_build_info,
+            self._avd_spec.android_efi_loader_build_info,
             self._avd_spec.ota_build_info,
             self._avd_spec.host_package_build_info)
         creds_cache_file = os.path.join(_HOME_FOLDER, cfg.creds_cache_file)
@@ -407,31 +463,61 @@
         logger.debug("cmd:\n %s", cmd)
         ssh.ShellCmdWithRetry(cmd)
 
+    @staticmethod
+    def _ReplaceRemoteImageArgs(launch_cvd_args, old_dir, new_dir):
+        """Replace the prefix of launch_cvd path arguments.
+
+        Args:
+            launch_cvd_args: A list of string pairs. Each pair consists of a
+                             launch_cvd option and a remote path.
+            old_dir: The prefix of the paths to be replaced.
+            new_dir: The new prefix of the paths.
+
+        Returns:
+            A list of string pairs, the replaced arguments.
+
+        Raises:
+            errors.CreateError if any path cannot be replaced.
+        """
+        if any(remote_path.isabs(path) != remote_path.isabs(old_dir) for
+               _, path in launch_cvd_args):
+            raise errors.CreateError(f"Cannot convert {launch_cvd_args} to "
+                                     f"relative paths under {old_dir}")
+        return [(option,
+                 remote_path.join(new_dir, remote_path.relpath(path, old_dir)))
+                for option, path in launch_cvd_args]
+
+    @utils.TimeExecute(function_description="Copying images")
+    def _CopyRemoteImageDir(self, remote_src_dir, remote_dst_dir):
+        """Copy a remote directory recursively.
+
+        Args:
+            remote_src_dir: The source directory.
+            remote_dst_dir: The destination directory.
+        """
+        self._ssh.Run(f"cp -frT {remote_src_dir} {remote_dst_dir}")
+
     @utils.TimeExecute(
         function_description="Launching AVD(s) and waiting for boot up",
         result_evaluator=utils.BootEvaluator)
-    def _LaunchCvd(self, image_args, start_time):
+    def _LaunchCvd(self, image_args, deadline):
         """Execute launch_cvd.
 
         Args:
             image_args: A list of strings, the extra arguments generated by
                         acloud for remote image paths.
-            start_time: The timestamp when the remote host is initialized.
+            deadline: The timestamp when the timeout expires.
 
         Returns:
             The error message as a string. An empty string represents success.
         """
-        self._compute_client.SetStage(constants.STAGE_BOOT_UP)
         config = cvd_utils.GetConfigFromRemoteAndroidInfo(
             self._ssh, self._GetArtifactPath())
         cmd = cvd_utils.GetRemoteLaunchCvdCmd(
             self._GetInstancePath(), self._avd_spec, config, image_args)
-        boot_timeout_secs = (self._avd_spec.boot_timeout_secs or
-                             constants.DEFAULT_CF_BOOT_TIMEOUT)
-        boot_timeout_secs -= time.time() - start_time
+        boot_timeout_secs = deadline - time.time()
         if boot_timeout_secs <= 0:
-            return ("Timed out before launch_cvd. "
-                    f"Remaining time: {boot_timeout_secs} secs.")
+            return "Timed out before launch_cvd."
 
         self._compute_client.ExtendReportData(
             constants.LAUNCH_CVD_COMMAND, cmd)
diff --git a/public/actions/remote_host_cf_device_factory_test.py b/public/actions/remote_host_cf_device_factory_test.py
index 057e0bc..89c4b05 100644
--- a/public/actions/remote_host_cf_device_factory_test.py
+++ b/public/actions/remote_host_cf_device_factory_test.py
@@ -18,10 +18,12 @@
 import unittest
 from unittest import mock
 
+from acloud import errors
 from acloud.internal import constants
 from acloud.internal.lib import driver_test_lib
 from acloud.public.actions import remote_host_cf_device_factory
 
+
 class RemoteHostDeviceFactoryTest(driver_test_lib.BaseDriverTest):
     """Test RemoteHostDeviceFactory."""
 
@@ -56,6 +58,7 @@
                          kernel_build_info={},
                          boot_build_info={},
                          bootloader_build_info={},
+                         android_efi_loader_build_info={},
                          ota_build_info={},
                          host_package_build_info={},
                          remote_host="192.0.2.100",
@@ -98,7 +101,7 @@
         mock_cvd_utils.GetRemoteHostBaseDir.return_value = "acloud_cf_2"
         mock_cvd_utils.FormatRemoteHostInstanceName.return_value = "inst"
         mock_cvd_utils.AreTargetFilesRequired.return_value = True
-        mock_cvd_utils.UploadExtraImages.return_value = ["extra"]
+        mock_cvd_utils.UploadExtraImages.return_value = [("-extra", "image")]
         mock_cvd_utils.ExecuteRemoteLaunchCvd.return_value = "failure"
         mock_cvd_utils.FindRemoteLogs.return_value = [log]
 
@@ -119,7 +122,7 @@
             mock_ssh_obj, "acloud_cf_2")
         # LaunchCvd
         mock_cvd_utils.GetRemoteLaunchCvdCmd.assert_called_with(
-            "acloud_cf_2", mock_avd_spec, mock.ANY, ["extra"])
+            "acloud_cf_2", mock_avd_spec, mock.ANY, ["-extra", "image"])
         mock_cvd_utils.ExecuteRemoteLaunchCvd.assert_called()
         # FindLogFiles
         mock_cvd_utils.FindRemoteLogs.assert_called_with(
@@ -385,6 +388,91 @@
         self.assertFalse(factory.GetFailures())
         self.assertDictEqual({"inst": [log]}, factory.GetLogs())
 
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory.ssh")
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+                "cvd_utils")
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+                "subprocess.check_call")
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory.glob")
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory.pull")
+    def testCreateInstanceWithRemoteImageDir(self, _mock_pull, mock_glob,
+                                             _mock_check_call, mock_cvd_utils,
+                                             mock_ssh):
+        """Test CreateInstance with AvdSpec.remote_image_dir."""
+        mock_avd_spec = self._CreateMockAvdSpec()
+        mock_avd_spec.remote_image_dir = "mock_img_dir"
+
+        mock_ssh_obj = mock.Mock()
+        mock_ssh.Ssh.return_value = mock_ssh_obj
+        # Test initializing the remote image dir.
+        mock_glob.glob.return_value = ["/mock/super.img"]
+        factory = remote_host_cf_device_factory.RemoteHostDeviceFactory(
+            mock_avd_spec)
+
+        mock_cvd_utils.GetRemoteHostBaseDir.return_value = "acloud_cf_1"
+        mock_cvd_utils.FormatRemoteHostInstanceName.return_value = "inst"
+        mock_cvd_utils.LoadRemoteImageArgs.return_value = None
+        mock_cvd_utils.AreTargetFilesRequired.return_value = False
+        mock_cvd_utils.UploadExtraImages.return_value = [
+            ("arg", "mock_img_dir/1")]
+        mock_cvd_utils.ExecuteRemoteLaunchCvd.return_value = ""
+        mock_cvd_utils.FindRemoteLogs.return_value = []
+
+        self._mock_build_api.GetFetchBuildArgs.return_value = ["-test"]
+
+        self.assertEqual("inst", factory.CreateInstance())
+        mock_cvd_utils.PrepareRemoteImageDirLink.assert_called_once_with(
+            mock_ssh_obj, "acloud_cf_1", "mock_img_dir")
+        mock_cvd_utils.LoadRemoteImageArgs.assert_called_once_with(
+            mock_ssh_obj, "mock_img_dir/acloud_image_timestamp.txt",
+            "mock_img_dir/acloud_image_args.txt", mock.ANY)
+        mock_cvd_utils.SaveRemoteImageArgs.assert_called_once_with(
+            mock_ssh_obj, "mock_img_dir/acloud_image_args.txt",
+            [("arg", "mock_img_dir/1")])
+        mock_ssh_obj.Run.assert_called_with("cp -frT mock_img_dir acloud_cf_1")
+        self._mock_build_api.DownloadFetchcvd.assert_called_once()
+        self.assertEqual(["arg", "acloud_cf_1/1"],
+                         mock_cvd_utils.GetRemoteLaunchCvdCmd.call_args[0][3])
+
+        # Test reusing the remote image dir.
+        mock_cvd_utils.LoadRemoteImageArgs.return_value = [
+            ["arg", "mock_img_dir/2"]]
+        mock_cvd_utils.SaveRemoteImageArgs.reset_mock()
+        mock_ssh_obj.reset_mock()
+        self._mock_build_api.DownloadFetchcvd.reset_mock()
+
+        self.assertEqual("inst", factory.CreateInstance())
+        mock_cvd_utils.SaveRemoteImageArgs.assert_not_called()
+        mock_ssh_obj.Run.assert_called_with("cp -frT mock_img_dir acloud_cf_1")
+        self._mock_build_api.DownloadFetchcvd.assert_not_called()
+        self.assertEqual(["arg", "acloud_cf_1/2"],
+                         mock_cvd_utils.GetRemoteLaunchCvdCmd.call_args[0][3])
+
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory.ssh")
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+                "cvd_utils")
+    @mock.patch("acloud.public.actions.remote_host_cf_device_factory."
+                "subprocess.check_call")
+    def testCreateInstanceWithCreateError(self, _mock_check_call,
+                                          mock_cvd_utils, mock_ssh):
+        """Test CreateInstance with CreateError."""
+        mock_avd_spec = self._CreateMockAvdSpec()
+        mock_avd_spec.remote_image_dir = "mock_img_dir"
+
+        mock_ssh_obj = mock.Mock()
+        mock_ssh.Ssh.return_value = mock_ssh_obj
+
+        mock_cvd_utils.GetRemoteHostBaseDir.return_value = "acloud_cf_1"
+        mock_cvd_utils.FormatRemoteHostInstanceName.return_value = "inst"
+        mock_cvd_utils.LoadRemoteImageArgs.side_effect = errors.CreateError(
+            "failure")
+        factory = remote_host_cf_device_factory.RemoteHostDeviceFactory(
+            mock_avd_spec)
+
+        self.assertEqual("inst", factory.CreateInstance())
+        self.assertEqual({"inst": "failure"}, factory.GetFailures())
+        mock_cvd_utils.ExecuteRemoteLaunchCvd.assert_not_called()
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/public/actions/remote_instance_cf_device_factory.py b/public/actions/remote_instance_cf_device_factory.py
index b625999..ba618c1 100644
--- a/public/actions/remote_instance_cf_device_factory.py
+++ b/public/actions/remote_instance_cf_device_factory.py
@@ -111,6 +111,7 @@
                     avd_spec.kernel_build_info,
                     avd_spec.boot_build_info,
                     avd_spec.bootloader_build_info,
+                    avd_spec.android_efi_loader_build_info,
                     avd_spec.ota_build_info,
                     avd_spec.host_package_build_info)
 
@@ -126,7 +127,7 @@
         if avd_spec.extra_files:
             self._compute_client.UploadExtraFiles(avd_spec.extra_files)
 
-        return launch_cvd_args
+        return [arg for arg_pair in launch_cvd_args for arg in arg_pair]
 
     @utils.TimeExecute(function_description="Downloading target_files archive")
     def _DownloadTargetFiles(self, download_dir):
diff --git a/public/actions/remote_instance_cf_device_factory_test.py b/public/actions/remote_instance_cf_device_factory_test.py
index ecd2485..9d736a2 100644
--- a/public/actions/remote_instance_cf_device_factory_test.py
+++ b/public/actions/remote_instance_cf_device_factory_test.py
@@ -243,7 +243,7 @@
         mock_cvd_utils.AreTargetFilesRequired.return_value = False
         mock_cvd_utils.FindRemoteLogs.return_value = [{"path": "/logcat"}]
         mock_cvd_utils.UploadExtraImages.return_value = [
-            "-boot_image", "/boot/img"]
+            ("-boot_image", "/boot/img")]
 
         fake_host_package_name = "/fake/host_package.tar.gz"
         fake_image_name = ""