diff --git a/fastboot/Android.bp b/fastboot/Android.bp
index 02a887ec8..a757d56e5 100644
--- a/fastboot/Android.bp
+++ b/fastboot/Android.bp
@@ -137,12 +137,14 @@ cc_binary {
         "libhidlbase",
         "liblog",
         "liblp",
+        "libprotobuf-cpp-lite",
         "libsparse",
         "libutils",
     ],
 
     static_libs: [
         "libhealthhalutils",
+        "libsnapshot_nobinder",
     ],
 
     header_libs: [
diff --git a/fastboot/device/commands.cpp b/fastboot/device/commands.cpp
index dfd569062..905578c4c 100644
--- a/fastboot/device/commands.cpp
+++ b/fastboot/device/commands.cpp
@@ -19,6 +19,8 @@
 #include <sys/socket.h>
 #include <sys/un.h>
 
+#include <unordered_set>
+
 #include <android-base/logging.h>
 #include <android-base/parseint.h>
 #include <android-base/properties.h>
@@ -33,6 +35,7 @@
 #include <libgsi/libgsi.h>
 #include <liblp/builder.h>
 #include <liblp/liblp.h>
+#include <libsnapshot/snapshot.h>
 #include <uuid/uuid.h>
 
 #include "constants.h"
@@ -48,6 +51,7 @@ using ::android::hardware::boot::V1_0::Slot;
 using ::android::hardware::boot::V1_1::MergeStatus;
 using ::android::hardware::fastboot::V1_0::Result;
 using ::android::hardware::fastboot::V1_0::Status;
+using android::snapshot::SnapshotManager;
 using IBootControl1_1 = ::android::hardware::boot::V1_1::IBootControl;
 
 struct VariableHandlers {
@@ -57,6 +61,24 @@ struct VariableHandlers {
     std::function<std::vector<std::vector<std::string>>(FastbootDevice*)> get_all_args;
 };
 
+static bool IsSnapshotUpdateInProgress(FastbootDevice* device) {
+    auto hal = device->boot1_1();
+    if (!hal) {
+        return false;
+    }
+    auto merge_status = hal->getSnapshotMergeStatus();
+    return merge_status == MergeStatus::SNAPSHOTTED || merge_status == MergeStatus::MERGING;
+}
+
+static bool IsProtectedPartitionDuringMerge(FastbootDevice* device, const std::string& name) {
+    static const std::unordered_set<std::string> ProtectedPartitionsDuringMerge = {
+            "userdata", "metadata", "misc"};
+    if (ProtectedPartitionsDuringMerge.count(name) == 0) {
+        return false;
+    }
+    return IsSnapshotUpdateInProgress(device);
+}
+
 static void GetAllVars(FastbootDevice* device, const std::string& name,
                        const VariableHandlers& handlers) {
     if (!handlers.get_all_args) {
@@ -142,8 +164,14 @@ bool EraseHandler(FastbootDevice* device, const std::vector<std::string>& args)
         return device->WriteStatus(FastbootResult::FAIL, "Erase is not allowed on locked devices");
     }
 
+    const auto& partition_name = args[1];
+    if (IsProtectedPartitionDuringMerge(device, partition_name)) {
+        auto message = "Cannot erase " + partition_name + " while a snapshot update is in progress";
+        return device->WriteFail(message);
+    }
+
     PartitionHandle handle;
-    if (!OpenPartition(device, args[1], &handle)) {
+    if (!OpenPartition(device, partition_name, &handle)) {
         return device->WriteStatus(FastbootResult::FAIL, "Partition doesn't exist");
     }
     if (wipe_block_device(handle.fd(), get_block_device_size(handle.fd())) == 0) {
@@ -208,9 +236,9 @@ bool SetActiveHandler(FastbootDevice* device, const std::vector<std::string>& ar
                                    "set_active command is not allowed on locked devices");
     }
 
-    // Slot suffix needs to be between 'a' and 'z'.
     Slot slot;
     if (!GetSlotNumber(args[1], &slot)) {
+        // Slot suffix needs to be between 'a' and 'z'.
         return device->WriteStatus(FastbootResult::FAIL, "Bad slot suffix");
     }
 
@@ -223,6 +251,32 @@ bool SetActiveHandler(FastbootDevice* device, const std::vector<std::string>& ar
     if (slot >= boot_control_hal->getNumberSlots()) {
         return device->WriteStatus(FastbootResult::FAIL, "Slot out of range");
     }
+
+    // If the slot is not changing, do nothing.
+    if (slot == boot_control_hal->getCurrentSlot()) {
+        return device->WriteOkay("");
+    }
+
+    // Check how to handle the current snapshot state.
+    if (auto hal11 = device->boot1_1()) {
+        auto merge_status = hal11->getSnapshotMergeStatus();
+        if (merge_status == MergeStatus::MERGING) {
+            return device->WriteFail("Cannot change slots while a snapshot update is in progress");
+        }
+        // Note: we allow the slot change if the state is SNAPSHOTTED. First-
+        // stage init does not have access to the HAL, and uses the slot number
+        // and /metadata OTA state to determine whether a slot change occurred.
+        // Booting into the old slot would erase the OTA, and switching A->B->A
+        // would simply resume it if no boots occur in between. Re-flashing
+        // partitions implicitly cancels the OTA, so leaving the state as-is is
+        // safe.
+        if (merge_status == MergeStatus::SNAPSHOTTED) {
+            device->WriteInfo(
+                    "Changing the active slot with a snapshot applied may cancel the"
+                    " update.");
+        }
+    }
+
     CommandResult ret;
     auto cb = [&ret](CommandResult result) { ret = result; };
     auto result = boot_control_hal->setActiveBootSlot(slot, cb);
@@ -466,6 +520,11 @@ bool FlashHandler(FastbootDevice* device, const std::vector<std::string>& args)
     }
 
     const auto& partition_name = args[1];
+    if (IsProtectedPartitionDuringMerge(device, partition_name)) {
+        auto message = "Cannot flash " + partition_name + " while a snapshot update is in progress";
+        return device->WriteFail(message);
+    }
+
     if (LogicalPartitionExists(device, partition_name)) {
         CancelPartitionSnapshot(device, partition_name);
     }
@@ -555,12 +614,9 @@ bool GsiHandler(FastbootDevice* device, const std::vector<std::string>& args) {
 bool SnapshotUpdateHandler(FastbootDevice* device, const std::vector<std::string>& args) {
     // Note that we use the HAL rather than mounting /metadata, since we want
     // our results to match the bootloader.
-    auto hal = device->boot_control_hal();
+    auto hal = device->boot1_1();
     if (!hal) return device->WriteFail("Not supported");
 
-    android::sp<IBootControl1_1> hal11 = IBootControl1_1::castFrom(hal);
-    if (!hal11) return device->WriteFail("Not supported");
-
     // If no arguments, return the same thing as a getvar. Note that we get the
     // HAL first so we can return "not supported" before we return the less
     // specific error message below.
@@ -573,18 +629,34 @@ bool SnapshotUpdateHandler(FastbootDevice* device, const std::vector<std::string
         return device->WriteOkay("");
     }
 
-    if (args.size() != 2 || args[1] != "cancel") {
+    MergeStatus status = hal->getSnapshotMergeStatus();
+
+    if (args.size() != 2) {
         return device->WriteFail("Invalid arguments");
     }
+    if (args[1] == "cancel") {
+        switch (status) {
+            case MergeStatus::SNAPSHOTTED:
+            case MergeStatus::MERGING:
+                hal->setSnapshotMergeStatus(MergeStatus::CANCELLED);
+                break;
+            default:
+                break;
+        }
+    } else if (args[1] == "merge") {
+        if (status != MergeStatus::MERGING) {
+            return device->WriteFail("No snapshot merge is in progress");
+        }
 
-    MergeStatus status = hal11->getSnapshotMergeStatus();
-    switch (status) {
-        case MergeStatus::SNAPSHOTTED:
-        case MergeStatus::MERGING:
-            hal11->setSnapshotMergeStatus(MergeStatus::CANCELLED);
-            break;
-        default:
-            break;
+        auto sm = SnapshotManager::NewForFirstStageMount();
+        if (!sm) {
+            return device->WriteFail("Unable to create SnapshotManager");
+        }
+        if (!sm->HandleImminentDataWipe()) {
+            return device->WriteFail("Unable to finish snapshot merge");
+        }
+    } else {
+        return device->WriteFail("Invalid parameter to snapshot-update");
     }
     return device->WriteStatus(FastbootResult::OKAY, "Success");
 }
diff --git a/fastboot/device/fastboot_device.cpp b/fastboot/device/fastboot_device.cpp
index d3c2bdaeb..31fc35937 100644
--- a/fastboot/device/fastboot_device.cpp
+++ b/fastboot/device/fastboot_device.cpp
@@ -60,7 +60,11 @@ FastbootDevice::FastbootDevice()
       boot_control_hal_(IBootControl::getService()),
       health_hal_(get_health_service()),
       fastboot_hal_(IFastboot::getService()),
-      active_slot_("") {}
+      active_slot_("") {
+    if (boot_control_hal_) {
+        boot1_1_ = android::hardware::boot::V1_1::IBootControl::castFrom(boot_control_hal_);
+    }
+}
 
 FastbootDevice::~FastbootDevice() {
     CloseDevice();
diff --git a/fastboot/device/fastboot_device.h b/fastboot/device/fastboot_device.h
index 091aadfd3..bbe817244 100644
--- a/fastboot/device/fastboot_device.h
+++ b/fastboot/device/fastboot_device.h
@@ -23,6 +23,7 @@
 #include <vector>
 
 #include <android/hardware/boot/1.0/IBootControl.h>
+#include <android/hardware/boot/1.1/IBootControl.h>
 #include <android/hardware/fastboot/1.0/IFastboot.h>
 #include <android/hardware/health/2.0/IHealth.h>
 
@@ -51,6 +52,7 @@ class FastbootDevice {
     android::sp<android::hardware::boot::V1_0::IBootControl> boot_control_hal() {
         return boot_control_hal_;
     }
+    android::sp<android::hardware::boot::V1_1::IBootControl> boot1_1() { return boot1_1_; }
     android::sp<android::hardware::fastboot::V1_0::IFastboot> fastboot_hal() {
         return fastboot_hal_;
     }
@@ -63,6 +65,7 @@ class FastbootDevice {
 
     std::unique_ptr<Transport> transport_;
     android::sp<android::hardware::boot::V1_0::IBootControl> boot_control_hal_;
+    android::sp<android::hardware::boot::V1_1::IBootControl> boot1_1_;
     android::sp<android::hardware::health::V2_0::IHealth> health_hal_;
     android::sp<android::hardware::fastboot::V1_0::IFastboot> fastboot_hal_;
     std::vector<char> download_data_;
diff --git a/fastboot/device/variables.cpp b/fastboot/device/variables.cpp
index 6e613d647..91fc84c47 100644
--- a/fastboot/device/variables.cpp
+++ b/fastboot/device/variables.cpp
@@ -432,19 +432,13 @@ bool GetSnapshotUpdateStatus(FastbootDevice* device, const std::vector<std::stri
                              std::string* message) {
     // Note that we use the HAL rather than mounting /metadata, since we want
     // our results to match the bootloader.
-    auto hal = device->boot_control_hal();
+    auto hal = device->boot1_1();
     if (!hal) {
         *message = "not supported";
         return false;
     }
 
-    android::sp<IBootControl1_1> hal11 = IBootControl1_1::castFrom(hal);
-    if (!hal11) {
-        *message = "not supported";
-        return false;
-    }
-
-    MergeStatus status = hal11->getSnapshotMergeStatus();
+    MergeStatus status = hal->getSnapshotMergeStatus();
     switch (status) {
         case MergeStatus::SNAPSHOTTED:
             *message = "snapshotted";
diff --git a/fastboot/fastboot.cpp b/fastboot/fastboot.cpp
index 7ce7c7c0e..cbd42b17e 100644
--- a/fastboot/fastboot.cpp
+++ b/fastboot/fastboot.cpp
@@ -399,6 +399,9 @@ static int show_help() {
             " snapshot-update cancel     On devices that support snapshot-based updates, cancel\n"
             "                            an in-progress update. This may make the device\n"
             "                            unbootable until it is reflashed.\n"
+            " snapshot-update merge      On devices that support snapshot-based updates, finish\n"
+            "                            an in-progress update if it is in the \"merging\"\n"
+            "                            phase.\n"
             "\n"
             "boot image:\n"
             " boot KERNEL [RAMDISK [SECOND]]\n"
@@ -2089,8 +2092,8 @@ int FastBootTool::Main(int argc, char* argv[]) {
             if (!args.empty()) {
                 arg = next_arg(&args);
             }
-            if (!arg.empty() && arg != "cancel") {
-                syntax_error("expected: snapshot-update [cancel]");
+            if (!arg.empty() && (arg != "cancel" && arg != "merge")) {
+                syntax_error("expected: snapshot-update [cancel|merge]");
             }
             fb->SnapshotUpdateCommand(arg);
         } else {
diff --git a/fastboot/fastboot_driver.cpp b/fastboot/fastboot_driver.cpp
index 6a5ad206a..8d534ea32 100644
--- a/fastboot/fastboot_driver.cpp
+++ b/fastboot/fastboot_driver.cpp
@@ -124,8 +124,11 @@ RetCode FastBootDriver::SetActive(const std::string& slot, std::string* response
 
 RetCode FastBootDriver::SnapshotUpdateCommand(const std::string& command, std::string* response,
                                               std::vector<std::string>* info) {
+    prolog_(StringPrintf("Snapshot %s", command.c_str()));
     std::string raw = FB_CMD_SNAPSHOT_UPDATE ":" + command;
-    return RawCommand(raw, response, info);
+    auto result = RawCommand(raw, response, info);
+    epilog_(result);
+    return result;
 }
 
 RetCode FastBootDriver::FlashPartition(const std::string& partition,
diff --git a/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h b/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h
index 8f247098f..7411e5a01 100644
--- a/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h
+++ b/fs_mgr/libsnapshot/include/libsnapshot/snapshot.h
@@ -224,7 +224,8 @@ class SnapshotManager final {
     bool CreateLogicalAndSnapshotPartitions(const std::string& super_device);
 
     // This method should be called preceding any wipe or flash of metadata or
-    // userdata. It is only valid in recovery.
+    // userdata. It is only valid in recovery or fastbootd, and it ensures that
+    // a merge has been completed.
     //
     // When userdata will be wiped or flashed, it is necessary to clean up any
     // snapshot state. If a merge is in progress, the merge must be finished.
diff --git a/fs_mgr/libsnapshot/snapshot.cpp b/fs_mgr/libsnapshot/snapshot.cpp
index 1de700861..008ece722 100644
--- a/fs_mgr/libsnapshot/snapshot.cpp
+++ b/fs_mgr/libsnapshot/snapshot.cpp
@@ -2185,6 +2185,15 @@ bool SnapshotManager::HandleImminentDataWipe(const std::function<void()>& callba
         return true;
     }
 
+    // Check this early, so we don't accidentally start trying to populate
+    // the state file in recovery. Note we don't call GetUpdateState since
+    // we want errors in acquiring the lock to be propagated, instead of
+    // returning UpdateState::None.
+    auto state_file = GetStateFilePath();
+    if (access(state_file.c_str(), F_OK) != 0 && errno == ENOENT) {
+        return true;
+    }
+
     auto slot_number = SlotNumberForSlotSuffix(device_->GetSlotSuffix());
     auto super_path = device_->GetSuperDevice(slot_number);
     if (!CreateLogicalAndSnapshotPartitions(super_path)) {