device-mapper-multipath-0.9.9-17

Add 0083-libmultipath-add-purge_disconnected-configuration-op.patch
Add 0084-multipathd-implement-purge-functionality-for-disconn.patch
  * Fixes RHEL-141287 ("Add purge_disconnected support to multipathd")
Resolves: RHEL-141287
This commit is contained in:
Benjamin Marzinski 2026-01-29 21:54:17 -05:00
parent 91c21a51b4
commit 420113ceaf
3 changed files with 1040 additions and 1 deletions

View File

@ -0,0 +1,279 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Brian Bunker <brian@purestorage.com>
Date: Fri, 9 Jan 2026 16:50:43 -0800
Subject: [PATCH] libmultipath: add purge_disconnected configuration option
Add a new configuration option 'purge_disconnected' that can be set
per multipath device, hardware entry, or globally. This option will
be used to control whether multipathd should automatically remove
paths that are in a disconnected state.
The option is disabled by default (PURGE_DISCONNECTED_OFF).
This patch only adds the configuration infrastructure. The actual
purge functionality will be implemented in a subsequent patch.
Signed-off-by: Brian Bunker <brian@purestorage.com>
Reviewed-by: Benjamin Marzinski <bmarzins@redhat.com>
Reviewed-by: Martin Wilck <mwilck@suse.com>
Signed-off-by: Benjamin Marzinski <bmarzins@redhat.com>
---
libmultipath/config.c | 2 ++
libmultipath/config.h | 3 +++
libmultipath/configure.c | 1 +
libmultipath/defaults.h | 1 +
libmultipath/dict.c | 14 ++++++++++++++
libmultipath/propsel.c | 16 ++++++++++++++++
libmultipath/propsel.h | 1 +
libmultipath/structs.h | 12 ++++++++++++
multipath/multipath.conf.5.in | 22 ++++++++++++++++++++++
9 files changed, 72 insertions(+)
diff --git a/libmultipath/config.c b/libmultipath/config.c
index 3d5943d3..5c87fbe0 100644
--- a/libmultipath/config.c
+++ b/libmultipath/config.c
@@ -470,6 +470,7 @@ merge_hwe (struct hwentry * dst, struct hwentry * src)
merge_num(marginal_path_err_rate_threshold);
merge_num(marginal_path_err_recheck_gap_time);
merge_num(marginal_path_double_failed_time);
+ merge_num(purge_disconnected);
snprintf(id, sizeof(id), "%s/%s", dst->vendor, dst->product);
reconcile_features_with_options(id, &dst->features,
@@ -517,6 +518,7 @@ merge_mpe(struct mpentry *dst, struct mpentry *src)
merge_num(skip_kpartx);
merge_num(max_sectors_kb);
merge_num(ghost_delay);
+ merge_num(purge_disconnected);
merge_num(uid);
merge_num(gid);
merge_num(mode);
diff --git a/libmultipath/config.h b/libmultipath/config.h
index 158cebf0..c9e4029e 100644
--- a/libmultipath/config.h
+++ b/libmultipath/config.h
@@ -89,6 +89,7 @@ struct hwentry {
int marginal_path_err_rate_threshold;
int marginal_path_err_recheck_gap_time;
int marginal_path_double_failed_time;
+ int purge_disconnected;
int skip_kpartx;
int max_sectors_kb;
int ghost_delay;
@@ -131,6 +132,7 @@ struct mpentry {
int marginal_path_err_rate_threshold;
int marginal_path_err_recheck_gap_time;
int marginal_path_double_failed_time;
+ int purge_disconnected;
int skip_kpartx;
int max_sectors_kb;
int ghost_delay;
@@ -189,6 +191,7 @@ struct config {
int marginal_path_err_rate_threshold;
int marginal_path_err_recheck_gap_time;
int marginal_path_double_failed_time;
+ int purge_disconnected;
int uxsock_timeout;
int strict_timing;
int retrigger_tries;
diff --git a/libmultipath/configure.c b/libmultipath/configure.c
index b5c701fa..e92c905a 100644
--- a/libmultipath/configure.c
+++ b/libmultipath/configure.c
@@ -355,6 +355,7 @@ int setup_map(struct multipath *mpp, char **params, struct vectors *vecs)
select_max_sectors_kb(conf, mpp);
select_ghost_delay(conf, mpp);
select_flush_on_last_del(conf, mpp);
+ select_purge_disconnected(conf, mpp);
sysfs_set_scsi_tmo(conf, mpp);
marginal_pathgroups = conf->marginal_pathgroups;
diff --git a/libmultipath/defaults.h b/libmultipath/defaults.h
index 3602636c..ed31e475 100644
--- a/libmultipath/defaults.h
+++ b/libmultipath/defaults.h
@@ -57,6 +57,7 @@
#define DEFAULT_ALL_TG_PT ALL_TG_PT_OFF
#define DEFAULT_RECHECK_WWID RECHECK_WWID_OFF
#define DEFAULT_AUTO_RESIZE AUTO_RESIZE_NEVER
+#define DEFAULT_PURGE_DISCONNECTED PURGE_DISCONNECTED_OFF
/* Enable no foreign libraries by default */
#define DEFAULT_ENABLE_FOREIGN "NONE"
diff --git a/libmultipath/dict.c b/libmultipath/dict.c
index 546103f2..968163d7 100644
--- a/libmultipath/dict.c
+++ b/libmultipath/dict.c
@@ -941,6 +941,16 @@ declare_hw_snprint(skip_kpartx, print_yes_no_undef)
declare_mp_handler(skip_kpartx, set_yes_no_undef)
declare_mp_snprint(skip_kpartx, print_yes_no_undef)
+declare_def_handler(purge_disconnected, set_yes_no_undef)
+declare_def_snprint_defint(purge_disconnected, print_yes_no_undef,
+ DEFAULT_PURGE_DISCONNECTED)
+declare_ovr_handler(purge_disconnected, set_yes_no_undef)
+declare_ovr_snprint(purge_disconnected, print_yes_no_undef)
+declare_hw_handler(purge_disconnected, set_yes_no_undef)
+declare_hw_snprint(purge_disconnected, print_yes_no_undef)
+declare_mp_handler(purge_disconnected, set_yes_no_undef)
+declare_mp_snprint(purge_disconnected, print_yes_no_undef)
+
declare_def_range_handler(remove_retries, 0, INT_MAX)
declare_def_snprint(remove_retries, print_int)
@@ -2227,6 +2237,7 @@ init_keywords(vector keywords)
install_keyword("retrigger_delay", &def_retrigger_delay_handler, &snprint_def_retrigger_delay);
install_keyword("missing_uev_wait_timeout", &def_uev_wait_timeout_handler, &snprint_def_uev_wait_timeout);
install_keyword("skip_kpartx", &def_skip_kpartx_handler, &snprint_def_skip_kpartx);
+ install_keyword("purge_disconnected", &def_purge_disconnected_handler, &snprint_def_purge_disconnected);
install_keyword("disable_changed_wwids", &deprecated_disable_changed_wwids_handler, &snprint_deprecated);
install_keyword("remove_retries", &def_remove_retries_handler, &snprint_def_remove_retries);
install_keyword("max_sectors_kb", &def_max_sectors_kb_handler, &snprint_def_max_sectors_kb);
@@ -2310,6 +2321,7 @@ init_keywords(vector keywords)
install_keyword("marginal_path_err_recheck_gap_time", &hw_marginal_path_err_recheck_gap_time_handler, &snprint_hw_marginal_path_err_recheck_gap_time);
install_keyword("marginal_path_double_failed_time", &hw_marginal_path_double_failed_time_handler, &snprint_hw_marginal_path_double_failed_time);
install_keyword("skip_kpartx", &hw_skip_kpartx_handler, &snprint_hw_skip_kpartx);
+ install_keyword("purge_disconnected", &hw_purge_disconnected_handler, &snprint_hw_purge_disconnected);
install_keyword("max_sectors_kb", &hw_max_sectors_kb_handler, &snprint_hw_max_sectors_kb);
install_keyword("ghost_delay", &hw_ghost_delay_handler, &snprint_hw_ghost_delay);
install_keyword("all_tg_pt", &hw_all_tg_pt_handler, &snprint_hw_all_tg_pt);
@@ -2355,6 +2367,7 @@ init_keywords(vector keywords)
install_keyword("marginal_path_double_failed_time", &ovr_marginal_path_double_failed_time_handler, &snprint_ovr_marginal_path_double_failed_time);
install_keyword("skip_kpartx", &ovr_skip_kpartx_handler, &snprint_ovr_skip_kpartx);
+ install_keyword("purge_disconnected", &ovr_purge_disconnected_handler, &snprint_ovr_purge_disconnected);
install_keyword("max_sectors_kb", &ovr_max_sectors_kb_handler, &snprint_ovr_max_sectors_kb);
install_keyword("ghost_delay", &ovr_ghost_delay_handler, &snprint_ovr_ghost_delay);
install_keyword("all_tg_pt", &ovr_all_tg_pt_handler, &snprint_ovr_all_tg_pt);
@@ -2400,6 +2413,7 @@ init_keywords(vector keywords)
install_keyword("marginal_path_err_recheck_gap_time", &mp_marginal_path_err_recheck_gap_time_handler, &snprint_mp_marginal_path_err_recheck_gap_time);
install_keyword("marginal_path_double_failed_time", &mp_marginal_path_double_failed_time_handler, &snprint_mp_marginal_path_double_failed_time);
install_keyword("skip_kpartx", &mp_skip_kpartx_handler, &snprint_mp_skip_kpartx);
+ install_keyword("purge_disconnected", &mp_purge_disconnected_handler, &snprint_mp_purge_disconnected);
install_keyword("max_sectors_kb", &mp_max_sectors_kb_handler, &snprint_mp_max_sectors_kb);
install_keyword("ghost_delay", &mp_ghost_delay_handler, &snprint_mp_ghost_delay);
install_sublevel_end();
diff --git a/libmultipath/propsel.c b/libmultipath/propsel.c
index 5ad0b78c..9b7aba43 100644
--- a/libmultipath/propsel.c
+++ b/libmultipath/propsel.c
@@ -1367,6 +1367,22 @@ out:
return 0;
}
+int select_purge_disconnected(struct config *conf, struct multipath *mp)
+{
+ const char *origin;
+
+ mp_set_mpe(purge_disconnected);
+ mp_set_ovr(purge_disconnected);
+ mp_set_hwe(purge_disconnected);
+ mp_set_conf(purge_disconnected);
+ mp_set_default(purge_disconnected, DEFAULT_PURGE_DISCONNECTED);
+out:
+ condlog(3, "%s: purge_disconnected = %s %s", mp->alias,
+ (mp->purge_disconnected == PURGE_DISCONNECTED_ON) ? "yes" : "no",
+ origin);
+ return 0;
+}
+
int select_max_sectors_kb(struct config *conf, struct multipath * mp)
{
const char *origin;
diff --git a/libmultipath/propsel.h b/libmultipath/propsel.h
index 73615c2f..ee83253e 100644
--- a/libmultipath/propsel.h
+++ b/libmultipath/propsel.h
@@ -37,6 +37,7 @@ int select_marginal_path_err_rate_threshold(struct config *conf, struct multipat
int select_marginal_path_err_recheck_gap_time(struct config *conf, struct multipath *mp);
int select_marginal_path_double_failed_time(struct config *conf, struct multipath *mp);
int select_ghost_delay(struct config *conf, struct multipath * mp);
+int select_purge_disconnected(struct config *conf, struct multipath *mp);
void reconcile_features_with_options(const char *id, char **features,
int* no_path_retry,
int *retain_hwhandler);
diff --git a/libmultipath/structs.h b/libmultipath/structs.h
index 2ff195f3..e3f2ffb9 100644
--- a/libmultipath/structs.h
+++ b/libmultipath/structs.h
@@ -186,6 +186,17 @@ enum auto_resize_state {
AUTO_RESIZE_GROW_SHRINK,
};
+/*
+ * purge_disconnected configuration option (per multipath device)
+ * Controls whether paths that become disconnected at the storage target
+ * should be automatically removed from the system via sysfs.
+ */
+enum purge_disconnected_states {
+ PURGE_DISCONNECTED_UNDEF = YNU_UNDEF,
+ PURGE_DISCONNECTED_OFF = YNU_NO, /* Don't purge */
+ PURGE_DISCONNECTED_ON = YNU_YES, /* Purge disconnected paths */
+};
+
#define PROTOCOL_UNSET -1
enum scsi_protocol {
@@ -453,6 +464,7 @@ struct multipath {
int ghost_delay;
int ghost_delay_tick;
int queue_mode;
+ int purge_disconnected;
uid_t uid;
gid_t gid;
mode_t mode;
diff --git a/multipath/multipath.conf.5.in b/multipath/multipath.conf.5.in
index 31740a1f..af85092e 100644
--- a/multipath/multipath.conf.5.in
+++ b/multipath/multipath.conf.5.in
@@ -1304,6 +1304,22 @@ The default is: \fBno\fR
.
.
.TP
+.B purge_disconnected
+If set to
+.I yes
+, multipathd will automatically remove devices that are in a disconnected state.
+A path is considered disconnected when the TUR (Test Unit Ready) path checker
+receives the SCSI sense code "LOGICAL UNIT NOT SUPPORTED" (sense key 0x5,
+ASC/ASCQ 0x25/0x00). This typically indicates that the LUN has been unmapped
+or is no longer presented by the storage array. This option helps clean up
+stale device entries that would otherwise remain in the system.
+.RS
+.TP
+The default is: \fBno\fR
+.RE
+.
+.
+.TP
.B disable_changed_wwids
(Deprecated) This option is not supported anymore, and will be ignored.
.RE
@@ -1601,6 +1617,8 @@ section:
.TP
.B skip_kpartx
.TP
+.B purge_disconnected
+.TP
.B max_sectors_kb
.TP
.B ghost_delay
@@ -1786,6 +1804,8 @@ section:
.TP
.B skip_kpartx
.TP
+.B purge_disconnected
+.TP
.B max_sectors_kb
.TP
.B ghost_delay
@@ -1870,6 +1890,8 @@ the values are taken from the \fIdevices\fR or \fIdefaults\fR sections:
.TP
.B skip_kpartx
.TP
+.B purge_disconnected
+.TP
.B max_sectors_kb
.TP
.B ghost_delay

View File

@ -0,0 +1,752 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Brian Bunker <brian@purestorage.com>
Date: Fri, 9 Jan 2026 16:50:43 -0800
Subject: [PATCH] multipathd: implement purge functionality for disconnected
paths
Implement automatic purging of paths that have been disconnected at the
storage target (e.g., LUN unmapped). This builds on the purge_disconnected
configuration option added in the previous patch.
This adds:
- New PATH_DISCONNECTED checker state to signal disconnection
- TUR checker support for detecting LUN NOT SUPPORTED (ASC/ASCQ 0x25/0x00)
- Purge thread (purgeloop) that removes paths via sysfs delete attribute
- State machine to track disconnection and delay purging
- Conversion of PATH_DISCONNECTED to PATH_DOWN for normal processing
The purge thread runs independently and processes paths that have been
marked for purging by the checker thread. Paths are only purged after
remaining disconnected for delay_wait_checks intervals to avoid removing
paths that are temporarily flapping.
Signed-off-by: Brian Bunker <brian@purestorage.com>
Signed-off-by: Krishna Kant <krishna.kant@purestorage.com>
Reviewed-by: Benjamin Marzinski <bmarzins@redhat.com>
Reviewed-by: Martin Wilck <mwilck@suse.com>
Signed-off-by: Benjamin Marzinski <bmarzins@redhat.com>
---
libmultipath/checkers.c | 2 +
libmultipath/checkers.h | 15 +-
libmultipath/checkers/tur.c | 10 ++
libmultipath/discovery.c | 17 ++
libmultipath/io_err_stat.c | 1 +
libmultipath/print.c | 2 +
libmultipath/structs.h | 14 ++
multipathd/Makefile | 2 +-
multipathd/main.c | 74 +++++++-
multipathd/purge.c | 326 ++++++++++++++++++++++++++++++++++++
multipathd/purge.h | 41 +++++
11 files changed, 496 insertions(+), 8 deletions(-)
create mode 100644 multipathd/purge.c
create mode 100644 multipathd/purge.h
diff --git a/libmultipath/checkers.c b/libmultipath/checkers.c
index fdb91e17..5eca2b44 100644
--- a/libmultipath/checkers.c
+++ b/libmultipath/checkers.c
@@ -41,6 +41,7 @@ static const char *checker_state_names[PATH_MAX_STATE] = {
[PATH_TIMEOUT] = "timeout",
[PATH_REMOVED] = "removed",
[PATH_DELAYED] = "delayed",
+ [PATH_DISCONNECTED] = "disconnected",
};
static LIST_HEAD(checkers);
@@ -344,6 +345,7 @@ static const char *generic_msg[CHECKER_GENERIC_MSGTABLE_SIZE] = {
[CHECKER_MSGID_DOWN] = " reports path is down",
[CHECKER_MSGID_GHOST] = " reports path is ghost",
[CHECKER_MSGID_UNSUPPORTED] = " doesn't support this device",
+ [CHECKER_MSGID_DISCONNECTED] = " no access to this device",
};
const char *checker_message(const struct checker *c)
diff --git a/libmultipath/checkers.h b/libmultipath/checkers.h
index ea1e8af6..a0df88db 100644
--- a/libmultipath/checkers.h
+++ b/libmultipath/checkers.h
@@ -65,6 +65,15 @@
* delay_watch_checks checks, when it comes back up again, it will not
* be marked as up until it has been up for delay_wait_checks checks.
* During this time, it is marked as "delayed"
+ *
+ * PATH_DISCONNECTED is a special ephemeral state used to signal that a path
+ * has been disconnected at the storage target (e.g., LUN unmapped). When a
+ * checker returns PATH_DISCONNECTED:
+ * 1. The path's pp->disconnected field is set to track purge state
+ * 2. The state is immediately converted to PATH_DOWN for normal processing
+ * 3. If purge_disconnected is enabled, the path will be removed via sysfs
+ * This state should never be stored in pp->state or pp->chkrstate; it exists
+ * only as a transient return value from checkers to trigger special handling.
*/
enum path_check_state {
PATH_WILD = 0,
@@ -77,6 +86,7 @@ enum path_check_state {
PATH_TIMEOUT,
PATH_REMOVED,
PATH_DELAYED,
+ PATH_DISCONNECTED, /* Ephemeral: mapped to PATH_DOWN */
PATH_MAX_STATE
};
@@ -112,9 +122,10 @@ enum {
CHECKER_MSGID_DOWN,
CHECKER_MSGID_GHOST,
CHECKER_MSGID_UNSUPPORTED,
+ CHECKER_MSGID_DISCONNECTED,
CHECKER_GENERIC_MSGTABLE_SIZE,
- CHECKER_FIRST_MSGID = 100, /* lowest msgid for checkers */
- CHECKER_MSGTABLE_SIZE = 100, /* max msg table size for checkers */
+ CHECKER_FIRST_MSGID = 100, /* lowest msgid for checkers */
+ CHECKER_MSGTABLE_SIZE = 100, /* max msg table size for checkers */
};
struct checker_class;
diff --git a/libmultipath/checkers/tur.c b/libmultipath/checkers/tur.c
index 2800446d..7fd9e959 100644
--- a/libmultipath/checkers/tur.c
+++ b/libmultipath/checkers/tur.c
@@ -196,6 +196,16 @@ retry:
*msgid = MSG_TUR_TRANSITIONING;
return PATH_PENDING;
}
+ } else if (key == 0x5) {
+ /* Illegal request */
+ if (asc == 0x25 && ascq == 0x00) {
+ /*
+ * LUN NOT SUPPORTED: unmapped at target.
+ * Signals pp->disconnected, becomes PATH_DOWN.
+ */
+ *msgid = CHECKER_MSGID_DISCONNECTED;
+ return PATH_DISCONNECTED;
+ }
}
*msgid = CHECKER_MSGID_DOWN;
return PATH_DOWN;
diff --git a/libmultipath/discovery.c b/libmultipath/discovery.c
index a1284e73..289c97c7 100644
--- a/libmultipath/discovery.c
+++ b/libmultipath/discovery.c
@@ -2461,8 +2461,25 @@ int pathinfo(struct path *pp, struct config *conf, int mask)
pp->state == PATH_UNCHECKED ||
pp->state == PATH_WILD)
pp->chkrstate = pp->state = newstate;
+ /*
+ * PATH_TIMEOUT and PATH_DISCONNECTED are ephemeral
+ * states that should never be stored in pp->state.
+ * Convert them to PATH_DOWN immediately.
+ */
if (pp->state == PATH_TIMEOUT)
pp->state = PATH_DOWN;
+ if (pp->state == PATH_DISCONNECTED) {
+ int purge_enabled = pp->mpp &&
+ pp->mpp->purge_disconnected ==
+ PURGE_DISCONNECTED_ON;
+ if (purge_enabled &&
+ pp->disconnected == NOT_DISCONNECTED) {
+ condlog(2, "%s: mark path for purge",
+ pp->dev);
+ pp->disconnected = DISCONNECTED_READY_FOR_PURGE;
+ }
+ pp->state = PATH_DOWN;
+ }
if (pp->state == PATH_UP && !pp->size) {
condlog(3, "%s: device size is 0, "
"path unusable", pp->dev);
diff --git a/libmultipath/io_err_stat.c b/libmultipath/io_err_stat.c
index 1c594451..ca23187d 100644
--- a/libmultipath/io_err_stat.c
+++ b/libmultipath/io_err_stat.c
@@ -397,6 +397,7 @@ static void account_async_io_state(struct io_err_stat_path *pp, int rc)
switch (rc) {
case PATH_DOWN:
case PATH_TIMEOUT:
+ case PATH_DISCONNECTED:
pp->io_err_nr++;
break;
case PATH_UNCHECKED:
diff --git a/libmultipath/print.c b/libmultipath/print.c
index fbed2dd5..1946728f 100644
--- a/libmultipath/print.c
+++ b/libmultipath/print.c
@@ -547,6 +547,8 @@ snprint_chk_state (struct strbuf *buff, const struct path * pp)
return append_strbuf_str(buff, "i/o timeout");
case PATH_DELAYED:
return append_strbuf_str(buff, "delayed");
+ case PATH_DISCONNECTED:
+ return append_strbuf_str(buff, "disconnected");
default:
return append_strbuf_str(buff, "undef");
}
diff --git a/libmultipath/structs.h b/libmultipath/structs.h
index e3f2ffb9..dc184a93 100644
--- a/libmultipath/structs.h
+++ b/libmultipath/structs.h
@@ -197,6 +197,18 @@ enum purge_disconnected_states {
PURGE_DISCONNECTED_ON = YNU_YES, /* Purge disconnected paths */
};
+/*
+ * Path disconnection state (per path)
+ * Tracks whether a path has been marked for purge and whether it's already queued.
+ */
+enum path_disconnected_state {
+ NOT_DISCONNECTED, /* Path is not disconnected */
+ DISCONNECTED_READY_FOR_PURGE, /* Path is disconnected and ready to be
+ queued for purge */
+ DISCONNECTED_QUEUED_FOR_PURGE, /* Path is disconnected and already
+ queued for purge */
+};
+
#define PROTOCOL_UNSET -1
enum scsi_protocol {
@@ -376,6 +388,8 @@ struct path {
int state;
int dmstate;
int chkrstate;
+ enum path_disconnected_state disconnected; /* Marked for purge due to
+ disconnection */
int failcount;
int priority;
int pgindex;
diff --git a/multipathd/Makefile b/multipathd/Makefile
index 61cf1af6..bedb09bc 100644
--- a/multipathd/Makefile
+++ b/multipathd/Makefile
@@ -36,7 +36,7 @@ endif
CLI_OBJS := multipathc.o cli.o
OBJS := main.o pidfile.o uxlsnr.o uxclnt.o cli.o cli_handlers.o waiter.o \
- dmevents.o init_unwinder.o
+ dmevents.o init_unwinder.o purge.o
ifeq ($(FPIN_SUPPORT),1)
OBJS += fpin_handlers.o
endif
diff --git a/multipathd/main.c b/multipathd/main.c
index e9165350..7cb57e3c 100644
--- a/multipathd/main.c
+++ b/multipathd/main.c
@@ -83,6 +83,7 @@
#include "dmevents.h"
#include "io_err_stat.h"
#include "foreign.h"
+#include "purge.h"
#include "../third-party/valgrind/drd.h"
#include "init_unwinder.h"
@@ -135,11 +136,11 @@ static bool __delayed_reconfig;
pid_t daemon_pid;
static pthread_mutex_t config_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t config_cond;
-static pthread_t check_thr, uevent_thr, uxlsnr_thr, uevq_thr, dmevent_thr,
- fpin_thr, fpin_consumer_thr;
-static bool check_thr_started, uevent_thr_started, uxlsnr_thr_started,
- uevq_thr_started, dmevent_thr_started, fpin_thr_started,
- fpin_consumer_thr_started;
+static pthread_t check_thr, purge_thr, uevent_thr, uxlsnr_thr, uevq_thr,
+ dmevent_thr, fpin_thr, fpin_consumer_thr;
+static bool check_thr_started, purge_thr_started, uevent_thr_started,
+ uxlsnr_thr_started, uevq_thr_started, dmevent_thr_started,
+ fpin_thr_started, fpin_consumer_thr_started;
static int pid_fd = -1;
static inline enum daemon_status get_running_state(void)
@@ -2484,6 +2485,28 @@ check_path (struct vectors * vecs, struct path * pp, unsigned int ticks)
if (newstate == PATH_REMOVED)
newstate = PATH_DOWN;
+ /*
+ * PATH_DISCONNECTED is an ephemeral state used to signal that a path
+ * has been disconnected at the storage target (LUN unmapped). We use
+ * it to set pp->disconnected for purge tracking, then immediately
+ * convert it to PATH_DOWN for normal path failure handling.
+ *
+ * This ensures PATH_DISCONNECTED never gets stored in pp->state or
+ * pp->chkrstate - it exists only as a transient signal from the
+ * checker to trigger special handling before becoming PATH_DOWN.
+ */
+ if (newstate == PATH_DISCONNECTED) {
+ if (pp->mpp &&
+ pp->mpp->purge_disconnected == PURGE_DISCONNECTED_ON &&
+ pp->disconnected == NOT_DISCONNECTED) {
+ condlog(2, "%s: mark (%s) path for purge", pp->dev,
+ checker_state_name(newstate));
+ pp->disconnected = DISCONNECTED_READY_FOR_PURGE;
+ }
+ /* Always convert to PATH_DOWN for normal processing */
+ newstate = PATH_DOWN;
+ }
+
if (newstate == PATH_WILD || newstate == PATH_UNCHECKED) {
condlog(2, "%s: unusable path (%s) - checker failed",
pp->dev, checker_state_name(newstate));
@@ -2800,6 +2823,7 @@ checkerloop (void *ap)
int num_paths = 0, strict_timing, rc = 0, i = 0;
unsigned int ticks = 0;
enum checker_state checker_state = CHECKER_STARTING;
+ LIST_HEAD(purge_list);
if (set_config_state(DAEMON_RUNNING) != DAEMON_RUNNING)
/* daemon shutdown */
@@ -2878,6 +2902,12 @@ unlock:
}
}
+ /*
+ * Cleanup handler to free purge_list if thread is cancelled.
+ * This prevents memory leaks during shutdown.
+ */
+ pthread_cleanup_push(cleanup_purge_list, &purge_list);
+
pthread_cleanup_push(cleanup_lock, &vecs->lock);
lock(&vecs->lock);
pthread_testcancel();
@@ -2886,6 +2916,11 @@ unlock:
missing_uev_wait_tick(vecs);
ghost_delay_tick(vecs);
partial_retrigger_tick(vecs->pathvec);
+ /*
+ * Build purge list for disconnected paths.
+ * The caller will queue it after releasing vecs->lock.
+ */
+ build_purge_list(vecs, &purge_list);
lock_cleanup_pop(vecs->lock);
if (count)
@@ -2900,6 +2935,26 @@ unlock:
lock_cleanup_pop(vecs->lock);
}
+ /*
+ * Queue purge work for disconnected paths.
+ * This is done after releasing vecs->lock to avoid holding
+ * the lock while signaling the purge thread.
+ */
+ if (!list_empty(&purge_list)) {
+ pthread_cleanup_push(cleanup_mutex, &purge_mutex);
+ pthread_mutex_lock(&purge_mutex);
+ pthread_testcancel();
+ list_splice_tail_init(&purge_list, &purge_queue);
+ pthread_cond_signal(&purge_cond);
+ pthread_cleanup_pop(1);
+ }
+
+ /*
+ * Pop cleanup handler. Execute it (arg=1) to free purge_list
+ * at the end of each iteration.
+ */
+ pthread_cleanup_pop(1);
+
get_monotonic_time(&end_time);
timespecsub(&end_time, &start_time, &diff_time);
if (num_paths) {
@@ -3396,6 +3451,8 @@ static void cleanup_threads(void)
if (check_thr_started)
pthread_cancel(check_thr);
+ if (purge_thr_started)
+ pthread_cancel(purge_thr);
if (uevent_thr_started)
pthread_cancel(uevent_thr);
if (uxlsnr_thr_started)
@@ -3412,6 +3469,8 @@ static void cleanup_threads(void)
if (check_thr_started)
pthread_join(check_thr, NULL);
+ if (purge_thr_started)
+ pthread_join(purge_thr, NULL);
if (uevent_thr_started)
pthread_join(uevent_thr, NULL);
if (uxlsnr_thr_started)
@@ -3664,6 +3723,11 @@ child (__attribute__((unused)) void *param)
goto failed;
} else
check_thr_started = true;
+ if ((rc = pthread_create(&purge_thr, &misc_attr, purgeloop, vecs))) {
+ condlog(0, "failed to create purge loop thread: %d", rc);
+ goto failed;
+ } else
+ purge_thr_started = true;
if ((rc = pthread_create(&uevq_thr, &misc_attr, uevqloop, vecs))) {
condlog(0, "failed to create uevent dispatcher: %d", rc);
goto failed;
diff --git a/multipathd/purge.c b/multipathd/purge.c
new file mode 100644
index 00000000..44f0c905
--- /dev/null
+++ b/multipathd/purge.c
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2025 Brian Bunker <brian@purestorage.com>
+ * Copyright (C) 2025 Krishna Kant <krishna.kant@purestorage.com>
+ */
+
+#include <pthread.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <libudev.h>
+#include <urcu.h>
+
+#include "vector.h"
+#include "structs.h"
+#include "structs_vec.h"
+#include "debug.h"
+#include "util.h"
+#include "lock.h"
+#include "sysfs.h"
+#include "list.h"
+#include "purge.h"
+
+pthread_mutex_t purge_mutex = PTHREAD_MUTEX_INITIALIZER;
+pthread_cond_t purge_cond = PTHREAD_COND_INITIALIZER;
+LIST_HEAD(purge_queue);
+
+/*
+ * Information needed to purge a path. We copy this data while holding
+ * vecs->lock, then release the lock before doing the actual sysfs write.
+ * This prevents blocking other operations while waiting for sysfs I/O.
+ *
+ * The udev device reference captures the sysfs path (including H:C:T:L).
+ * The duplicated fd prevents device name/number reuse: the kernel will not
+ * reuse the device's minor number (which maps to the device name) for a new
+ * device while we hold an open file descriptor, even if the original device
+ * has been removed. This protects against deleting a new device that reused
+ * the same name after the original was removed externally.
+ */
+struct purge_path_info {
+ struct list_head node; /* List linkage */
+ struct udev_device *udev; /* Udev device (refcounted) */
+ int fd; /* Dup'd fd prevents device reuse */
+};
+
+/*
+ * Attempt to delete a path by writing to the SCSI device's sysfs delete
+ * attribute. This triggers kernel-level device removal. The actual cleanup
+ * of the path structure from pathvec happens later when a uevent arrives
+ * (handled by uev_remove_path).
+ *
+ * This function does NOT require vecs->lock to be held, as it operates on
+ * copied data. This function may block while writing to sysfs, which is
+ * why it's called without holding any locks.
+ *
+ * Protection against device reuse:
+ * The duplicated fd in purge_path_info prevents the kernel from reusing
+ * the device's minor number (and thus the device name like /dev/sdd) for
+ * a new device, even if the original device has been removed externally.
+ * This ensures we cannot accidentally delete a new device that reused the
+ * same name. The kernel maintains this guarantee as long as we hold the
+ * open file descriptor.
+ */
+static void delete_path_sysfs(struct purge_path_info *info)
+{
+ struct udev_device *ud;
+ const char *devname;
+
+ if (!info->udev)
+ goto out;
+
+ devname = udev_device_get_devnode(info->udev);
+
+ /*
+ * Get the SCSI device parent. This is where we'll write to the
+ * "delete" attribute to trigger device removal.
+ */
+ ud = udev_device_get_parent_with_subsystem_devtype(info->udev, "scsi",
+ "scsi_device");
+ if (!ud) {
+ condlog(3, "%s: failed to purge, no SCSI parent found", devname);
+ goto out;
+ }
+
+ /*
+ * Write "1" to the SCSI device's delete attribute to trigger
+ * kernel-level device removal.
+ */
+ if (sysfs_attr_set_value(ud, "delete", "1", 1) < 0)
+ condlog(3, "%s: failed to purge", devname);
+ else
+ condlog(2, "%s: purged", devname);
+
+out:
+ return;
+}
+
+/*
+ * Prepare purge info for a path while holding vecs->lock.
+ * Takes a reference on the udev device and duplicates the fd.
+ * Returns allocated purge_path_info on success, NULL on failure.
+ *
+ * We require a valid fd because it prevents the kernel from reusing
+ * the device's minor number (and device name) for a new device while
+ * we hold it open. This protects against accidentally deleting a new
+ * device that reused the same name after the original was removed.
+ */
+static struct purge_path_info *prepare_purge_path_info(struct path *pp)
+{
+ struct purge_path_info *info = NULL;
+
+ if (!pp->udev || !pp->mpp)
+ goto out;
+
+ /*
+ * We require a valid fd to prevent device name reuse.
+ * Without it, we cannot safely purge the device.
+ */
+ if (pp->fd < 0) {
+ condlog(3, "%s: no fd available, cannot safely purge", pp->dev);
+ goto out;
+ }
+
+ info = calloc(1, sizeof(*info));
+ if (!info)
+ goto out;
+
+ INIT_LIST_HEAD(&info->node);
+ info->udev = udev_device_ref(pp->udev);
+ if (!info->udev)
+ goto out_free;
+
+ info->fd = dup(pp->fd);
+ if (info->fd < 0) {
+ condlog(3, "%s: failed to dup fd: %s, cannot safely purge",
+ pp->dev, strerror(errno));
+ goto out_unref;
+ }
+
+ return info;
+
+out_unref:
+ udev_device_unref(info->udev);
+out_free:
+ free(info);
+ info = NULL;
+out:
+ return info;
+}
+
+/*
+ * Clean up and free purge info.
+ */
+static void free_purge_path_info(struct purge_path_info *info)
+{
+ if (!info)
+ return;
+
+ if (info->fd >= 0)
+ close(info->fd);
+ if (info->udev)
+ udev_device_unref(info->udev);
+ free(info);
+}
+
+/*
+ * Build a list of purge_path_info for all paths marked for purge.
+ * This should be called while holding vecs->lock. It clears the
+ * disconnected flag and prepares purge info for each path, adding
+ * them to tmpq.
+ */
+void build_purge_list(struct vectors *vecs, struct list_head *tmpq)
+{
+ struct path *pp;
+ unsigned int i;
+
+ vector_foreach_slot (vecs->pathvec, pp, i) {
+ struct purge_path_info *info;
+
+ if (pp->disconnected != DISCONNECTED_READY_FOR_PURGE)
+ continue;
+
+ /*
+ * Mark as queued whether we succeed or fail.
+ * On success, we're purging it now.
+ * On failure, retrying is unlikely to help until
+ * the checker re-evaluates the path.
+ */
+ pp->disconnected = DISCONNECTED_QUEUED_FOR_PURGE;
+
+ info = prepare_purge_path_info(pp);
+ if (info) {
+ condlog(2, "%s: queuing path for purge", pp->dev);
+ list_add_tail(&info->node, tmpq);
+ } else
+ condlog(3, "%s: failed to prepare purge info", pp->dev);
+ }
+}
+
+static void rcu_unregister(__attribute__((unused)) void *param)
+{
+ rcu_unregister_thread();
+}
+
+/*
+ * Cleanup handler for a single purge_path_info.
+ * Used to prevent memory leaks if thread is cancelled while processing.
+ */
+static void cleanup_purge_path_info(void *arg)
+{
+ struct purge_path_info *info = arg;
+
+ free_purge_path_info(info);
+}
+
+/*
+ * Cleanup handler for purge list. Frees all purge_path_info entries.
+ * Can be called as a pthread cleanup handler or directly.
+ */
+void cleanup_purge_list(void *arg)
+{
+ struct list_head *purge_list = arg;
+ struct purge_path_info *info, *tmp;
+
+ list_for_each_entry_safe(info, tmp, purge_list, node)
+ {
+ list_del_init(&info->node);
+ free_purge_path_info(info);
+ }
+}
+
+/*
+ * Cleanup handler for the global purge queue.
+ * Used during shutdown to free any remaining queued items.
+ */
+static void cleanup_global_purge_queue(void *arg __attribute__((unused)))
+{
+ pthread_mutex_lock(&purge_mutex);
+ cleanup_purge_list(&purge_queue);
+ pthread_mutex_unlock(&purge_mutex);
+}
+
+/*
+ * Main purge thread loop.
+ *
+ * This thread waits for purge_path_info structs to be queued by the checker
+ * thread, then processes them by writing to their sysfs delete attributes.
+ * The checker thread builds the list while holding vecs->lock, so this
+ * thread doesn't need to grab that lock at all.
+ *
+ * Uses list_splice_tail_init() like uevent_dispatch() to safely transfer
+ * items from the global queue to a local list for processing.
+ *
+ * Cleanup handlers are registered for both the local purge_list and the
+ * global purge_queue (similar to uevent_listen), and for each individual
+ * purge_path_info after it's popped off the list (similar to service_uevq).
+ * This ensures no memory leaks if the thread is cancelled at any point.
+ */
+void *purgeloop(void *ap __attribute__((unused)))
+{
+ pthread_cleanup_push(rcu_unregister, NULL);
+ rcu_register_thread();
+ mlockall(MCL_CURRENT | MCL_FUTURE);
+
+ /*
+ * Cleanup handler for global purge_queue.
+ * This handles items that were queued but not yet moved to purge_list.
+ */
+ pthread_cleanup_push(cleanup_global_purge_queue, NULL);
+
+ while (1) {
+ LIST_HEAD(purge_list);
+ struct purge_path_info *info;
+
+ /*
+ * Cleanup handler for local purge_list.
+ * This handles items that were moved from purge_queue but
+ * not yet processed.
+ */
+ pthread_cleanup_push(cleanup_purge_list, &purge_list);
+
+ /*
+ * Cleanup handler for purge_mutex.
+ * Note: pthread_cond_wait() reacquires the mutex before
+ * returning, even on cancellation, so this cleanup handler
+ * will properly unlock it if we're cancelled.
+ */
+ pthread_cleanup_push(cleanup_mutex, &purge_mutex);
+ pthread_mutex_lock(&purge_mutex);
+ pthread_testcancel();
+ while (list_empty(&purge_queue)) {
+ condlog(4, "purgeloop waiting for work");
+ pthread_cond_wait(&purge_cond, &purge_mutex);
+ }
+ list_splice_tail_init(&purge_queue, &purge_list);
+ pthread_cleanup_pop(1);
+
+ /*
+ * Process all paths in the list without holding any locks.
+ * The sysfs operations may block, but that's fine since we're
+ * not holding vecs->lock.
+ *
+ * After popping each info off the list, we immediately push
+ * a cleanup handler for it. This ensures it gets freed even
+ * if we're cancelled inside delete_path_sysfs().
+ */
+ while ((info = list_pop_entry(&purge_list, typeof(*info), node))) {
+ pthread_cleanup_push(cleanup_purge_path_info, info);
+ delete_path_sysfs(info);
+ pthread_cleanup_pop(1);
+ }
+
+ /*
+ * Pop cleanup handler without executing it (0) since we've
+ * already freed everything above. The handler only runs if
+ * the thread is cancelled during processing.
+ */
+ pthread_cleanup_pop(0);
+ }
+
+ pthread_cleanup_pop(1);
+ pthread_cleanup_pop(1);
+ return NULL;
+}
diff --git a/multipathd/purge.h b/multipathd/purge.h
new file mode 100644
index 00000000..1fe755f3
--- /dev/null
+++ b/multipathd/purge.h
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2025 Brian Bunker <brian@purestorage.com>
+ * Copyright (C) 2025 Krishna Kant <krishna.kant@purestorage.com>
+ */
+
+#ifndef PURGE_H_INCLUDED
+#define PURGE_H_INCLUDED
+
+#include <pthread.h>
+#include "list.h"
+
+struct vectors;
+
+/*
+ * Purge thread synchronization.
+ * The checker thread builds a list of paths to purge and queues them here.
+ * The purge thread picks up the queue and processes it.
+ */
+extern pthread_mutex_t purge_mutex;
+extern pthread_cond_t purge_cond;
+extern struct list_head purge_queue;
+
+/*
+ * Build a list of paths to purge and add them to tmpq. Called by checker
+ * thread while holding vecs->lock.
+ */
+void build_purge_list(struct vectors *vecs, struct list_head *tmpq);
+
+/*
+ * Cleanup handler for purge list. Frees all purge_path_info entries.
+ * Can be called as a pthread cleanup handler or directly for shutdown cleanup.
+ */
+void cleanup_purge_list(void *arg);
+
+/*
+ * Main purge thread loop
+ */
+void *purgeloop(void *ap);
+
+#endif /* PURGE_H_INCLUDED */

View File

@ -1,6 +1,6 @@
Name: device-mapper-multipath
Version: 0.9.9
Release: 16%{?dist}
Release: 17%{?dist}
Summary: Tools to manage multipath devices using device-mapper
License: GPLv2
URL: http://christophe.varoqui.free.fr/
@ -92,6 +92,8 @@ Patch0079: 0079-mpathpersist-Fix-REPORT-CAPABILITIES-output.patch
Patch0080: 0080-multipath-tools-update-NFINIDAT-InfiniBox-config-in-.patch
Patch0081: 0081-multipathd-make-multipathd-show-status-busy-checker-.patch
Patch0082: 0082-multipathd-print-path-offline-message-even-without-a.patch
Patch0083: 0083-libmultipath-add-purge_disconnected-configuration-op.patch
Patch0084: 0084-multipathd-implement-purge-functionality-for-disconn.patch
# runtime
Requires: %{name}-libs = %{version}-%{release}
@ -301,6 +303,12 @@ fi
%{_pkgconfdir}/libdmmp.pc
%changelog
* Thu Jan 29 2026 Benjamin Marzinski <bmarzins@redhat.com> - 0.9.9-17
- Add 0083-libmultipath-add-purge_disconnected-configuration-op.patch
- Add 0084-multipathd-implement-purge-functionality-for-disconn.patch
* Fixes RHEL-141287 ("Add purge_disconnected support to multipathd")
- Resolves: RHEL-141287
* Thu Jan 22 2026 Benjamin Marzinski <bmarzins@redhat.com> - 0.9.9-16
- Add 0081-multipathd-make-multipathd-show-status-busy-checker-.patch
* Fixes RHEL-136405 ("improve Busy checking for multipathd show status