Summary
The activating VDI sm_config key that blktap2 uses as a pool-wide activation lock has no crash-safety: it is released in a finally clause, which protects against Python exceptions but not against the host dying mid-activation (HA fence, power loss, watchdog, sysrq reset). The leaked key then persists in the pool database forever, failing every subsequent RW activation of that VDI pool-wide with MAP_DUPLICATE_KEY, and it cannot be cleared with xe (sm-config is a read-only map in the CLI) nor by xe-toolstack-restart.
Observed incident
After chaos testing that hard-reset hosts during VM activity (lab pool, LVMoISCSI SR, XCP-ng 8.x), six VDIs were left with sm_config = activating: True and no activation in flight anywhere. Every subsequent start of the affected VMs failed on every host:
Error code: SR_BACKEND_FAILURE_46
The VDI is not available [opterr=['MAP_DUPLICATE_KEY', 'VDI', 'sm_config', 'OpaqueRef:...', 'activating']]
xe vdi-param-remove cannot clear it: VDI sm-config is exposed by the CLI with only get/get_map handlers (xapi-cli-server/records.ml), so it fails with Cannot remove parameters from read-only map (cli_operations.ml).
xe-toolstack-restart does not clear it: the key lives in the pool database.
- The only remedy is raw API surgery from dom0:
session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'activating').
Mechanism
VDI._add_tag() (libs/sm/blktap2.py) implements the activation lock through the xapi pool DB:
try:
self._session.xenapi.VDI.add_to_sm_config(
vdi_ref, 'activating', 'True')
except XenAPI.Failure as e:
if e.details[0] == 'MAP_DUPLICATE_KEY' and not writable:
# Someone else is activating - a retry might succeed
return False
raise # RW attach (every VM start): hard fail
host_key = "host_%s" % host_ref
...
self._session.xenapi.VDI.add_to_sm_config(vdi_ref, host_key, attach_mode)
and the release is a finally in _activate_locked():
finally:
vdi_ref = self._session.xenapi.VDI.get_by_uuid(vdi_uuid)
self._session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'activating')
If the host dies between the add and the remove, the key stays behind. The key carries no owner identity and no timestamp, so nothing can arbitrate staleness afterwards — in explicit contrast to the sibling host_<host_ref> attach keys, which encode the owning host and get crash-recovery treatment in two places, both of which ignore activating:
- SM side:
resetvdis.reset_sr(), run on every sr_attach (PBD plug), clears the re-attaching host's host_* key and paused, but not activating.
- xapi side:
refresh_local_vdi_activations (storage_access.ml, called from dbsync_slave.ml at xapi startup) clears host_* keys owned by the restarting host or by hosts no longer in the pool, but not activating.
This combination is why the observed leak signature is exactly activating: True with no host_ key: after the crashed host reboots, its host_* key is cleaned up, and activating is orphaned forever.
Note the RO path is not meaningfully better off: MAP_DUPLICATE_KEY on a RO attach returns False, but the retry loop in activate() gives up after ATTACH_DETACH_RETRY_SECS and raises VDI locked.
A leaked key also breaks GC/coalesce
cleanup.py:_tagChildrenForRelink() waits 15×2s for activating to clear before tagging a leaf for relink, then raises SMException("Failed to tag vdi %s for relink"). So a leaked activating also permanently prevents leaf-coalesce on that VDI chain — silent snapshot-chain growth in addition to the visible VM start failures.
Reproduction
- Pool with a shared SR (LVMoISCSI or any blktap2-activated type), one VM.
- Start the VM; during the activation window on the executing host, hard-reset that host:
xe vm-start uuid=$VM & then on the host, within ~1s: echo b > /proc/sysrq-trigger
(Looping start/reset catches the window within tens of iterations; chaos testing catches it reliably. The blktap_activate_inject_failure/blktap_activate_error_handling FIST points can be used to widen the window.)
- After the host returns:
xe vdi-param-get uuid=$VDI param-name=sm-config → activating: True, no host_ key.
xe vm-start uuid=$VM → SR_BACKEND_FAILURE_46 / MAP_DUPLICATE_KEY, on every host, forever.
Impact
Any host crash during any VM start/attach on a shared SR can permanently block that VM's disks pool-wide until manual raw-API surgery, and permanently stall coalesce for the affected chains. HA deployments hit this naturally: the fence HA performs is exactly the reset that leaks the lock of whatever was activating at that moment.
Proposed fix
Give activating the same owner encoding and crash-recovery treatment as the host_* keys, without renaming the key (the GC's DB_VDI_ACTIVATING check and the snapshot sm_config filter in LVHDSR.py depend on the literal name):
_add_tag() stores the owning host_ref as the value (instead of 'True').
resetvdis.reset_sr() (already run on every sr_attach) removes an activating entry owned by the re-attaching host — a crashed host cleans up after itself as soon as it replugs its PBDs, before anything can attempt an activation.
resetvdis.reset_vdi() removes the entry under --force, or when the recorded owner is no longer part of the pool — making resetvdis single <uuid> a supported remedy for hosts that never come back.
Legacy bare 'True' values written by older code cannot be attributed to an owner and are only cleared by --force (no behaviour change for mixed pools during rolling upgrade).
An alternative considered — arbitrating at acquisition time on MAP_DUPLICATE_KEY by stealing the key when no host_* key is present — has a live race: a concurrent starter can observe the window between the two adjacent add_to_sm_config calls of a healthy activation and steal a live lock, allowing two RW tapdisks on shared storage. The owner-encoded cleanup above avoids that entirely.
Summary
The
activatingVDIsm_configkey that blktap2 uses as a pool-wide activation lock has no crash-safety: it is released in afinallyclause, which protects against Python exceptions but not against the host dying mid-activation (HA fence, power loss, watchdog, sysrq reset). The leaked key then persists in the pool database forever, failing every subsequent RW activation of that VDI pool-wide withMAP_DUPLICATE_KEY, and it cannot be cleared withxe(sm-config is a read-only map in the CLI) nor byxe-toolstack-restart.Observed incident
After chaos testing that hard-reset hosts during VM activity (lab pool, LVMoISCSI SR, XCP-ng 8.x), six VDIs were left with
sm_config=activating: Trueand no activation in flight anywhere. Every subsequent start of the affected VMs failed on every host:xe vdi-param-removecannot clear it: VDIsm-configis exposed by the CLI with onlyget/get_maphandlers (xapi-cli-server/records.ml), so it fails withCannot remove parameters from read-only map(cli_operations.ml).xe-toolstack-restartdoes not clear it: the key lives in the pool database.session.xenapi.VDI.remove_from_sm_config(vdi_ref, 'activating').Mechanism
VDI._add_tag()(libs/sm/blktap2.py) implements the activation lock through the xapi pool DB:and the release is a
finallyin_activate_locked():If the host dies between the add and the remove, the key stays behind. The key carries no owner identity and no timestamp, so nothing can arbitrate staleness afterwards — in explicit contrast to the sibling
host_<host_ref>attach keys, which encode the owning host and get crash-recovery treatment in two places, both of which ignoreactivating:resetvdis.reset_sr(), run on everysr_attach(PBD plug), clears the re-attaching host'shost_*key andpaused, but notactivating.refresh_local_vdi_activations(storage_access.ml, called fromdbsync_slave.mlat xapi startup) clearshost_*keys owned by the restarting host or by hosts no longer in the pool, but notactivating.This combination is why the observed leak signature is exactly
activating: Truewith nohost_key: after the crashed host reboots, itshost_*key is cleaned up, andactivatingis orphaned forever.Note the RO path is not meaningfully better off:
MAP_DUPLICATE_KEYon a RO attach returnsFalse, but the retry loop inactivate()gives up afterATTACH_DETACH_RETRY_SECSand raisesVDI locked.A leaked key also breaks GC/coalesce
cleanup.py:_tagChildrenForRelink()waits 15×2s foractivatingto clear before tagging a leaf for relink, then raisesSMException("Failed to tag vdi %s for relink"). So a leakedactivatingalso permanently prevents leaf-coalesce on that VDI chain — silent snapshot-chain growth in addition to the visible VM start failures.Reproduction
xe vm-start uuid=$VM &then on the host, within ~1s:echo b > /proc/sysrq-trigger(Looping start/reset catches the window within tens of iterations; chaos testing catches it reliably. The
blktap_activate_inject_failure/blktap_activate_error_handlingFIST points can be used to widen the window.)xe vdi-param-get uuid=$VDI param-name=sm-config→activating: True, nohost_key.xe vm-start uuid=$VM→SR_BACKEND_FAILURE_46/MAP_DUPLICATE_KEY, on every host, forever.Impact
Any host crash during any VM start/attach on a shared SR can permanently block that VM's disks pool-wide until manual raw-API surgery, and permanently stall coalesce for the affected chains. HA deployments hit this naturally: the fence HA performs is exactly the reset that leaks the lock of whatever was activating at that moment.
Proposed fix
Give
activatingthe same owner encoding and crash-recovery treatment as thehost_*keys, without renaming the key (the GC'sDB_VDI_ACTIVATINGcheck and the snapshot sm_config filter inLVHDSR.pydepend on the literal name):_add_tag()stores the owninghost_refas the value (instead of'True').resetvdis.reset_sr()(already run on everysr_attach) removes anactivatingentry owned by the re-attaching host — a crashed host cleans up after itself as soon as it replugs its PBDs, before anything can attempt an activation.resetvdis.reset_vdi()removes the entry under--force, or when the recorded owner is no longer part of the pool — makingresetvdis single <uuid>a supported remedy for hosts that never come back.Legacy bare
'True'values written by older code cannot be attributed to an owner and are only cleared by--force(no behaviour change for mixed pools during rolling upgrade).An alternative considered — arbitrating at acquisition time on
MAP_DUPLICATE_KEYby stealing the key when nohost_*key is present — has a live race: a concurrent starter can observe the window between the two adjacentadd_to_sm_configcalls of a healthy activation and steal a live lock, allowing two RW tapdisks on shared storage. The owner-encoded cleanup above avoids that entirely.