diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
index b581d34565..883f10d22d 100644
--- a/src/qemu/qemu_driver.c
+++ b/src/qemu/qemu_driver.c
@@ -14906,38 +14906,43 @@ qemuDomainBlockPivot(virConnectPtr conn,
          virSecurityManagerSetDiskLabel(driver->securityManager, vm->def, disk) < 0))
         goto cleanup;
 
-    /* Attempt the pivot.  */
+    /* Attempt the pivot.  Record the attempt now, to prevent duplicate
+     * attempts; but the actual disk change will be made when emitting
+     * the event.
+     * XXX On libvirtd restarts, if we missed the qemu event, we need
+     * to double check what state qemu is in.
+     * XXX We should be using qemu's rerror flag to make sure the job
+     * remains alive until we know it's final state.
+     * XXX If the abort command is synchronous but the qemu event says
+     * that pivot failed, we need to reflect that failure into the
+     * overall return value.  */
+    disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_PIVOT;
     qemuDomainObjEnterMonitor(driver, vm);
     ret = qemuMonitorDrivePivot(priv->mon, device, disk->mirror->path, format);
     qemuDomainObjExitMonitor(driver, vm);
 
-    if (ret == 0) {
-        /* XXX We want to revoke security labels and disk lease, as
-         * well as audit that revocation, before dropping the original
-         * source.  But it gets tricky if both source and mirror share
-         * common backing files (we want to only revoke the non-shared
-         * portion of the chain, and is made more difficult by the
-         * fact that we aren't tracking the full chain ourselves; so
-         * for now, we leak the access to the original.  */
-        virStorageSourceFree(oldsrc);
-        oldsrc = NULL;
-    } else {
+    if (ret < 0) {
         /* On failure, qemu abandons the mirror, and reverts back to
          * the source disk (RHEL 6.3 has a bug where the revert could
          * cause catastrophic failure in qemu, but we don't need to
          * worry about it here as it is not an upstream qemu problem.  */
         /* XXX should we be parsing the exact qemu error, or calling
          * 'query-block', to see what state we really got left in
-         * before killing the mirroring job?  And just as on the
-         * success case, there's security labeling to worry about.  */
+         * before killing the mirroring job?
+         * XXX We want to revoke security labels and disk lease, as
+         * well as audit that revocation, before dropping the original
+         * source.  But it gets tricky if both source and mirror share
+         * common backing files (we want to only revoke the non-shared
+         * portion of the chain); so for now, we leak the access to
+         * the original.  */
         virStorageSourceFree(disk->mirror);
+        disk->mirror = NULL;
+        disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
     }
-
-    disk->mirror = NULL;
-    disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
+    if (virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm) < 0)
+        ret = -1;
 
  cleanup:
-    /* revert to original disk def on failure */
     if (oldsrc)
         disk->src = oldsrc;
 
@@ -14980,6 +14985,8 @@ qemuDomainBlockJobImpl(virDomainObjPtr vm,
     unsigned int baseIndex = 0;
     char *basePath = NULL;
     char *backingPath = NULL;
+    virQEMUDriverConfigPtr cfg = virQEMUDriverGetConfig(driver);
+    bool save = false;
 
     if (!virDomainObjIsActive(vm)) {
         virReportError(VIR_ERR_OPERATION_INVALID, "%s",
@@ -15033,19 +15040,30 @@ qemuDomainBlockJobImpl(virDomainObjPtr vm,
                        disk->dst);
         goto endjob;
     }
-    if (mode == BLOCK_JOB_ABORT &&
-        (flags & VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT) &&
-        !(async && disk->mirror)) {
-        virReportError(VIR_ERR_OPERATION_INVALID,
-                       _("pivot of disk '%s' requires an active copy job"),
-                       disk->dst);
-        goto endjob;
-    }
+    if (mode == BLOCK_JOB_ABORT) {
+        if ((flags & VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT) &&
+            !(async && disk->mirror)) {
+            virReportError(VIR_ERR_OPERATION_INVALID,
+                           _("pivot of disk '%s' requires an active copy job"),
+                           disk->dst);
+            goto endjob;
+        }
+        if (disk->mirrorState != VIR_DOMAIN_DISK_MIRROR_STATE_NONE &&
+            disk->mirrorState != VIR_DOMAIN_DISK_MIRROR_STATE_READY) {
+            virReportError(VIR_ERR_OPERATION_INVALID,
+                           _("another job on disk '%s' is still being ended"),
+                           disk->dst);
+            goto endjob;
+        }
 
-    if (disk->mirror && mode == BLOCK_JOB_ABORT &&
-        (flags & VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT)) {
-        ret = qemuDomainBlockPivot(conn, driver, vm, device, disk);
-        goto waitjob;
+        if (disk->mirror && (flags & VIR_DOMAIN_BLOCK_JOB_ABORT_PIVOT)) {
+            ret = qemuDomainBlockPivot(conn, driver, vm, device, disk);
+            goto waitjob;
+        }
+        if (disk->mirror) {
+            disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_ABORT;
+            save = true;
+        }
     }
 
     if (base &&
@@ -15084,36 +15102,40 @@ qemuDomainBlockJobImpl(virDomainObjPtr vm,
     ret = qemuMonitorBlockJob(priv->mon, device, basePath, backingPath,
                               bandwidth, info, mode, async);
     qemuDomainObjExitMonitor(driver, vm);
-    if (ret < 0)
+    if (ret < 0) {
+        if (mode == BLOCK_JOB_ABORT && disk->mirror)
+            disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
         goto endjob;
+    }
 
     /* Snoop block copy operations, so future cancel operations can
      * avoid checking if pivot is safe.  */
     if (mode == BLOCK_JOB_INFO && ret == 1 && disk->mirror &&
-        info->cur == info->end && info->type == VIR_DOMAIN_BLOCK_JOB_TYPE_COPY)
+        info->cur == info->end && !disk->mirrorState) {
         disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_READY;
-
-    /* A successful block job cancelation stops any mirroring.  */
-    if (mode == BLOCK_JOB_ABORT && disk->mirror) {
-        /* XXX We should also revoke security labels and disk lease on
-         * the mirror, and audit that fact, before dropping things.  */
-        virStorageSourceFree(disk->mirror);
-        disk->mirror = NULL;
-        disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
+        save = true;
     }
 
  waitjob:
+    /* If we have made changes to XML due to a copy job, make a best
+     * effort to save it now.  But we can ignore failure, since there
+     * will be further changes when the event marks completion.  */
+    if (save)
+        ignore_value(virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm));
+
     /* With synchronous block cancel, we must synthesize an event, and
      * we silently ignore the ABORT_ASYNC flag.  With asynchronous
-     * block cancel, the event will come from qemu, but without the
-     * ABORT_ASYNC flag, we must block to guarantee synchronous
-     * operation.  We do the waiting while still holding the VM job,
-     * to prevent newly scheduled block jobs from confusing us.  */
+     * block cancel, the event will come from qemu and will update the
+     * XML as appropriate, but without the ABORT_ASYNC flag, we must
+     * block to guarantee synchronous operation.  We do the waiting
+     * while still holding the VM job, to prevent newly scheduled
+     * block jobs from confusing us.  */
     if (mode == BLOCK_JOB_ABORT) {
         if (!async) {
             /* Older qemu that lacked async reporting also lacked
-             * active commit, so we can hardcode the event to pull.
-             * We have to generate two variants of the event. */
+             * blockcopy and active commit, so we can hardcode the
+             * event to pull, and we know the XML doesn't need
+             * updating.  We have to generate two event variants.  */
             int type = VIR_DOMAIN_BLOCK_JOB_TYPE_PULL;
             int status = VIR_DOMAIN_BLOCK_JOB_CANCELED;
             event = virDomainEventBlockJobNewFromObj(vm, disk->src->path, type,
@@ -15121,6 +15143,8 @@ qemuDomainBlockJobImpl(virDomainObjPtr vm,
             event2 = virDomainEventBlockJob2NewFromObj(vm, disk->dst, type,
                                                        status);
         } else if (!(flags & VIR_DOMAIN_BLOCK_JOB_ABORT_ASYNC)) {
+            /* XXX If the event reports failure, we should reflect
+             * that back into the return status of this API call.  */
             while (1) {
                 /* Poll every 50ms */
                 static struct timespec ts = { .tv_sec = 0,
@@ -15158,6 +15182,7 @@ qemuDomainBlockJobImpl(virDomainObjPtr vm,
     }
 
  cleanup:
+    virObjectUnref(cfg);
     VIR_FREE(basePath);
     VIR_FREE(backingPath);
     VIR_FREE(device);
@@ -15389,6 +15414,10 @@ qemuDomainBlockCopy(virDomainObjPtr vm,
     disk->mirror = mirror;
     mirror = NULL;
 
+    if (virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm) < 0)
+        VIR_WARN("Unable to save status on vm %s after state change",
+                 vm->def->name);
+
  endjob:
     if (need_unlink && unlink(dest))
         VIR_WARN("unable to unlink just-created %s", dest);
diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c
index c66326539e..d0132e3b66 100644
--- a/src/qemu/qemu_process.c
+++ b/src/qemu/qemu_process.c
@@ -1016,6 +1016,8 @@ qemuProcessHandleBlockJob(qemuMonitorPtr mon ATTRIBUTE_UNUSED,
     virObjectEventPtr event2 = NULL;
     const char *path;
     virDomainDiskDefPtr disk;
+    virQEMUDriverConfigPtr cfg = virQEMUDriverGetConfig(driver);
+    bool save = false;
 
     virObjectLock(vm);
     disk = qemuProcessFindDomainDiskByAlias(vm, diskAlias);
@@ -1027,29 +1029,52 @@ qemuProcessHandleBlockJob(qemuMonitorPtr mon ATTRIBUTE_UNUSED,
         event = virDomainEventBlockJobNewFromObj(vm, path, type, status);
         event2 = virDomainEventBlockJob2NewFromObj(vm, disk->dst, type,
                                                    status);
-        /* XXX If we completed a block pull or commit, then recompute
-         * the cached backing chain to match.  Better would be storing
-         * the chain ourselves rather than reprobing, but this
-         * requires modifying domain_conf and our XML to fully track
-         * the chain across libvirtd restarts.  For that matter, if
-         * qemu gains support for committing the active layer, we have
-         * to update disk->src.  */
-        if ((type == VIR_DOMAIN_BLOCK_JOB_TYPE_PULL ||
-             type == VIR_DOMAIN_BLOCK_JOB_TYPE_COMMIT) &&
-            status == VIR_DOMAIN_BLOCK_JOB_COMPLETED)
+
+        /* If we completed a block pull or commit, then update the XML
+         * to match.  */
+        if (status == VIR_DOMAIN_BLOCK_JOB_COMPLETED) {
+            if (disk->mirrorState == VIR_DOMAIN_DISK_MIRROR_STATE_PIVOT) {
+                /* XXX We want to revoke security labels and disk
+                 * lease, as well as audit that revocation, before
+                 * dropping the original source.  But it gets tricky
+                 * if both source and mirror share common backing
+                 * files (we want to only revoke the non-shared
+                 * portion of the chain); so for now, we leak the
+                 * access to the original.  */
+                virStorageSourceFree(disk->src);
+                disk->src = disk->mirror;
+            } else {
+                virStorageSourceFree(disk->mirror);
+            }
+
+            /* Recompute the cached backing chain to match our
+             * updates.  Better would be storing the chain ourselves
+             * rather than reprobing, but we haven't quite completed
+             * that conversion to use our XML tracking. */
+            disk->mirror = NULL;
+            save = disk->mirrorState != VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
+            disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
             qemuDomainDetermineDiskChain(driver, vm, disk, true);
-        if (disk->mirror && type == VIR_DOMAIN_BLOCK_JOB_TYPE_COPY) {
+        } else if (disk->mirror && type == VIR_DOMAIN_BLOCK_JOB_TYPE_COPY) {
             if (status == VIR_DOMAIN_BLOCK_JOB_READY) {
                 disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_READY;
+                save = true;
             } else if (status == VIR_DOMAIN_BLOCK_JOB_FAILED) {
                 virStorageSourceFree(disk->mirror);
                 disk->mirror = NULL;
                 disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE;
+                save = true;
             }
         }
     }
 
+    if (save) {
+        if (virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm) < 0)
+            VIR_WARN("Unable to save status on vm %s after block job",
+                     vm->def->name);
+    }
     virObjectUnlock(vm);
+    virObjectUnref(cfg);
 
     if (event)
         qemuDomainEventQueue(driver, event);