libmultipath: fix leaked device-mapper suspend after a failed remove - #167
suresh-thelkar wants to merge 2 commits into
Conversation
…moval 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 <suresh.thelkar@yahoo.com>
…emove 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 <suresh.thelkar@yahoo.com>
|
Hi @mwilck @bmarzins — flagging this for review when you have a chance. Small two-commit fix in Happy to send the series to Thanks! |
Summary
Under a heavy device-mapper ioctl load (for example LUN-number recycling combined with ALUA transitions),
dm_flush_map__()could leave a multipath map suspended indefinitely after a failedDM_DEVICE_REMOVE.A suspended map wedges any later task that touches it in uninterruptible
Dstate, and becausesync()walks every block device, this hangs I/O node-wide until reboot.
This PR fixes two distinct defects on the failed-remove path in
libmultipath/devmapper.c.Background
Observed in production on a 6.6 kernel with a SAN multipath device under storage-reconfiguration churn that
triggers transient DM ioctl failures — LUN reassignment (
LUN assignments … have changed) combined with ALUAtransitions. Storage itself was healthy — zero I/O errors and no application I/O in flight at teardown. The
defect is entirely in user space: multipath-tools left the map suspended after a failed remove. A
subsequent page-cache writeback then submitted a bio to the suspended map, which device-mapper holds without
completing or failing (expected behavior while suspended); the writeback task kept the folio lock and blocked
forever in
Dstate, andsync_bdevs()spread the hang node-wide.Problem
When
DM_DEVICE_REMOVEfails, the map has already been suspended earlier indm_flush_map__(). The old codethen:
Misclassified query errors as external removal. It treated anything that was not
DM_IS_MPATH_YESas"removed externally" and returned
DM_FLUSH_OKwithout resuming. Butdm_is_mpath()also returnsDM_IS_MPATH_ERR(-1) when the status query itself transiently fails under ioctl pressure. That errorwas read as a successful external removal, and the still-suspended map was abandoned.
Issued a fire-and-forget resume. Even on the path that did attempt recovery, it issued a single
DM_DEVICE_RESUMEwhose result was ignored and never verified. If that resume did not take effect, the mapstayed suspended.
Fix
Two commits:
libmultipath: dm_flush_map__: do not treat query error as external removalOnly
DM_IS_MPATH_NOnow counts as "removed externally". ADM_IS_MPATH_ERRquery result is logged andfalls through to the resume path instead of being reported as success.
libmultipath: dm_flush_map__: verify the map resumed after a failed removeAdd
dm_resume_and_verify(), which re-issuesDM_DEVICE_RESUMEand confirms withdm_is_suspended()thatthe map is really active, retrying once. This is the same verify-resume pattern already used by
dm_addmap_reload(), now applied to the flush/teardown path. If the map is still suspended after theretry,
dm_flush_map__()returnsDM_FLUSH_FAIL(still restoringqueue_if_no_path) instead ofreporting success, and a
DM_IS_MPATH_ERRresult no longer keeps retrying as if the map were present.dm_resume_and_verify()also treats the case where another process removed the map between the failed removeand the resume (
suspended < 0 && dm_is_mpath() == DM_IS_MPATH_NO) as success, since that is not a leakedsuspend.
Impact
DM_FLUSH_FAIL) so callers and monitoring can react, ratherthan being silently masked as success.
Files changed
libmultipath/devmapper.cTesting
makeproducesmultipath,multipathd,multipathc, and alllibraries with no errors or warnings.
libmultipath/devmapper.orecompiles warning-free under the project's flags.Deterministic reproducer
Validated with a small
LD_PRELOADfault injector (dmfault.so) that, for a single test map, forcesDM_DEV_REMOVE → EBUSYand the followingDM_TABLE_STATUS → error(sodm_is_mpath()returns-1) — theexact misclassification condition. A fake
scsi_debugdisk is used (all real disks blacklisted); the injectedflush runs inside the
multipath -fCLI process and the map's suspend state is then asserted.Patched build → PASS:
Evidence mapping:
forcing DM_DEV_REMOVE EBUSY for mpathaforcing DM_TABLE_STATUS error ... (dm_is_mpath -> -1)DM_IS_MPATH_ERRmisclassification triggermpatha: unable to verify map state after failed remove-1no longer treated as "removed"; falls through to resume (thiscondlogdid not exist before)dmsetup info -o suspended mpatha = 'Active'dm_resume_and_verify()re-resumed and confirmed activeFor contrast, an unpatched current release under the same injection leaves the map
Suspendedand theharness reports
[ FAIL ] map LEFT SUSPENDED after failed remove -> BUG REPRODUCED (unpatched), confirmingthe defect is still present on the latest release.
dmfault.c— fault injector (full source)Click to expand
dmfault.c— build withgcc -shared -fPIC -o dmfault.so dmfault.c -ldlrepro-dm-suspend.sh— test harness (full source)Builds a multipath map over a fake
scsi_debugdisk (all real disks blacklisted, so it never touchesreal storage), runs
multipath -funder the injector, and asserts the map is not left suspended.Click to expand
repro-dm-suspend.sh