From 4ce8775802bf2c618fd8631fc73633f6b5ec8b96 Mon Sep 17 00:00:00 2001 From: Suresh Thelkar Date: Wed, 30 Sep 2026 04:18:41 +0000 Subject: [PATCH 1/2] libmultipath: dm_flush_map__: do not treat query error as external removal On a failed DM_DEVICE_REMOVE, dm_flush_map__() classified anything that was not DM_IS_MPATH_YES as "removed externally" and returned DM_FLUSH_OK without resuming the map it had just suspended. dm_is_mpath() also returns DM_IS_MPATH_ERR (-1) when the status query itself fails, which happens transiently under a heavy ioctl load (e.g. LUN-number recycling plus ALUA transitions). A transient error was thus misread as a successful external removal, and the map was left suspended indefinitely, wedging later I/O node-wide via sync(). Only DM_IS_MPATH_NO now counts as "removed externally". A query error is logged and falls through to the resume path instead of being reported as success. Signed-off-by: Suresh Thelkar --- libmultipath/devmapper.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/libmultipath/devmapper.c b/libmultipath/devmapper.c index e3870f597..a5570a856 100644 --- a/libmultipath/devmapper.c +++ b/libmultipath/devmapper.c @@ -1042,13 +1042,26 @@ int dm_flush_map__ (const char *mapname, int flags, int retries) } condlog(4, "multipath map %s removed", mapname); return DM_FLUSH_OK; - } else if (dm_is_mpath(mapname) != DM_IS_MPATH_YES) { - condlog(4, "multipath map %s removed externally", - mapname); - return DM_FLUSH_OK; /* raced. someone else removed it */ } else { - condlog(2, "failed to remove multipath map %s", - mapname); + int is_mpath = dm_is_mpath(mapname); + + if (is_mpath == DM_IS_MPATH_NO) { + condlog(4, "multipath map %s removed externally", + mapname); + return DM_FLUSH_OK; /* raced. someone else removed it */ + } + /* + * A query error (DM_IS_MPATH_ERR) must not be treated + * as "removed externally": under an ioctl storm the + * status query can transiently fail, and returning + * DM_FLUSH_OK here would leave the map suspended. + */ + if (is_mpath == DM_IS_MPATH_ERR) + condlog(1, "%s: unable to verify map state after failed remove", + mapname); + else + condlog(2, "failed to remove multipath map %s", + mapname); if ((flags & DMFL_SUSPEND) && queue_if_no_path != -1) { dm_simplecmd_noflush(DM_DEVICE_RESUME, mapname, udev_flags); From 2598cdc32317f10b59c26801671568dd9b725afc Mon Sep 17 00:00:00 2001 From: Suresh Thelkar Date: Wed, 30 Sep 2026 04:19:54 +0000 Subject: [PATCH 2/2] libmultipath: dm_flush_map__: verify the map resumed after a failed remove After a failed DM_DEVICE_REMOVE the map is left suspended and the code issued a single fire-and-forget DM_DEVICE_RESUME whose result was ignored and never verified. If that resume did not take effect (again possible under an ioctl storm) the map stayed suspended, which wedges any later task in uninterruptible D state and, because sync() walks every block device, hangs I/O node-wide until reboot. Add dm_resume_and_verify(), which re-issues DM_DEVICE_RESUME and confirms with dm_is_suspended() that the map is really active, retrying once. This is the same verify-resume pattern already used by dm_addmap_reload(), applied to the flush/teardown path. If the map is still suspended after the retry, dm_flush_map__() now returns DM_FLUSH_FAIL (still restoring queue_if_no_path) instead of reporting success, and a DM_IS_MPATH_ERR query result no longer keeps retrying as if the map were present. Signed-off-by: Suresh Thelkar --- libmultipath/devmapper.c | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/libmultipath/devmapper.c b/libmultipath/devmapper.c index a5570a856..465015e85 100644 --- a/libmultipath/devmapper.c +++ b/libmultipath/devmapper.c @@ -446,6 +446,35 @@ int dm_simplecmd_noflush (int task, const char *name, uint16_t udev_flags) return dm_simplecmd(task, name, DMFL_NO_FLUSH|DMFL_NEED_SYNC, udev_flags); } +/* + * A failed resume can leave the map suspended without the ioctl reporting + * failure. Mirror dm_addmap_reload(): re-issue DM_DEVICE_RESUME and confirm + * with dm_is_suspended() that the map is really active, rather than trusting + * the ioctl return value alone. Retry once to absorb a transient hiccup. + */ +static bool dm_resume_and_verify(const char *mapname, uint16_t udev_flags) +{ + int i; + + for (i = 0; i < 2; i++) { + int r = dm_simplecmd_noflush(DM_DEVICE_RESUME, mapname, udev_flags); + int suspended = dm_is_suspended(mapname); + + if (suspended == 0) + return true; + /* + * The map may have been removed by another process after the + * failed remove was observed; that is not a leaked suspend. + */ + if (suspended < 0 && dm_is_mpath(mapname) == DM_IS_MPATH_NO) + return true; + condlog(r ? 2 : 1, "%s: resume attempt %d did not activate the map", + mapname, i + 1); + } + condlog(0, "%s: map remains suspended after resume attempts", mapname); + return false; +} + static int dm_device_remove (const char *name, int flags) { return dm_simplecmd(DM_DEVICE_REMOVE, name, flags, 0); @@ -1062,19 +1091,31 @@ int dm_flush_map__ (const char *mapname, int flags, int retries) else condlog(2, "failed to remove multipath map %s", mapname); - if ((flags & DMFL_SUSPEND) && queue_if_no_path != -1) { - dm_simplecmd_noflush(DM_DEVICE_RESUME, - mapname, udev_flags); + if ((flags & DMFL_SUSPEND) && queue_if_no_path != -1 && + !dm_resume_and_verify(mapname, udev_flags)) { + r = DM_FLUSH_FAIL; + goto out; + } + /* + * The map state could not be verified, so do not keep + * retrying as if the map were still present; report the + * failure so callers/monitoring can act. + */ + if (is_mpath == DM_IS_MPATH_ERR) { + r = DM_FLUSH_FAIL; + goto out; } } if (retries) sleep(1); } while (retries-- > 0); + r = DM_FLUSH_FAIL; +out: if (queue_if_no_path == 1 && _dm_queue_if_no_path(mapname, 1) != 0) return DM_FLUSH_FAIL_CANT_RESTORE; - return DM_FLUSH_FAIL; + return r; } int