From 72db61e69748d98cbe371de3681e8ea3caec3cf6 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Tue, 19 May 2026 18:46:49 -0400 Subject: [PATCH] import UBI device-mapper-multipath-0.9.9-18.el10 --- ...update-NFINIDAT-InfiniBox-config-in-.patch | 64 ++ ...multipathd-show-status-busy-checker-.patch | 63 ++ ...-path-offline-message-even-without-a.patch | 36 + ...-purge_disconnected-configuration-op.patch | 279 +++++++ ...ment-purge-functionality-for-disconn.patch | 752 ++++++++++++++++++ ...t-fix-register-retry-status-checking.patch | 47 ++ ...ber-number-of-registered-keys-when-i.patch | 59 ++ ...fix-code-for-skipping-multipathd-pat.patch | 461 +++++++++++ device-mapper-multipath.spec | 58 +- 9 files changed, 1812 insertions(+), 7 deletions(-) create mode 100644 0080-multipath-tools-update-NFINIDAT-InfiniBox-config-in-.patch create mode 100644 0081-multipathd-make-multipathd-show-status-busy-checker-.patch create mode 100644 0082-multipathd-print-path-offline-message-even-without-a.patch create mode 100644 0083-libmultipath-add-purge_disconnected-configuration-op.patch create mode 100644 0084-multipathd-implement-purge-functionality-for-disconn.patch create mode 100644 0085-libmpathpersist-fix-register-retry-status-checking.patch create mode 100644 0086-multipathd-remember-number-of-registered-keys-when-i.patch create mode 100644 0087-libmpathpersist-fix-code-for-skipping-multipathd-pat.patch diff --git a/0080-multipath-tools-update-NFINIDAT-InfiniBox-config-in-.patch b/0080-multipath-tools-update-NFINIDAT-InfiniBox-config-in-.patch new file mode 100644 index 0000000..f2c81c7 --- /dev/null +++ b/0080-multipath-tools-update-NFINIDAT-InfiniBox-config-in-.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Xose Vazquez Perez +Date: Sat, 16 Aug 2025 20:16:23 +0200 +Subject: [PATCH] multipath-tools: update NFINIDAT/InfiniBox config in hwtable + +New recommended values for SLES-15, RHEL-8, and Ubuntu-22, or above: +https://lh3.googleusercontent.com/pw/AP1GczMchJ6bcMIohp_g3Ik5DS6BZv_IW0iwaEXf968sJsR8fr_p3mR4ThRpmHpZE_VMnlcV8j0uuuI2kz-eoqekRCUBxyTBKS7n-4WFMsUiecq8i7nIjEuhfZFrV54DBQLDzGt6ofakAAF1L-ZcAuCWW18=w0-h0 + +device { + vendor "NFINIDAT" + product "InfiniBox" + path_grouping_policy "group_by_prio" + path_checker "tur" + features 0 + hardware_handler "1 alua" + prio "alua" + rr_weight "priorities" + no_path_retry "queue" + rr_min_io 1 + rr_min_io_rq 1 + flush_on_last_del "yes" + fast_io_fail_tmo 15 + dev_loss_tmo "infinity" + path_selector "service-time 0" + failback "immediate" + detect_prio "no" + user_friendly_names "no" + } + +Cc: Martin Wilck +Cc: Benjamin Marzinski +Cc: Christophe Varoqui +Cc: DM_DEVEL-ML +Signed-off-by: Xose Vazquez Perez +Reviewed-by: Martin Wilck +Signed-off-by: Benjamin Marzinski +--- + libmultipath/hwtable.c | 11 +++++------ + 1 file changed, 5 insertions(+), 6 deletions(-) + +diff --git a/libmultipath/hwtable.c b/libmultipath/hwtable.c +index 17ba5b45..689cd8da 100644 +--- a/libmultipath/hwtable.c ++++ b/libmultipath/hwtable.c +@@ -1153,14 +1153,13 @@ static struct hwentry default_hw[] = { + .vendor = "NFINIDAT", + .product = "InfiniBox", + .pgpolicy = GROUP_BY_PRIO, +- .pgfailback = 30, ++ .pgfailback = -FAILBACK_IMMEDIATE, + .prio_name = PRIO_ALUA, +- .selector = "round-robin 0", +- .rr_weight = RR_WEIGHT_PRIO, +- .no_path_retry = NO_PATH_RETRY_FAIL, +- .minio = 1, +- .minio_rq = 1, ++ .no_path_retry = NO_PATH_RETRY_QUEUE, ++ .flush_on_last_del = FLUSH_ALWAYS, + .fast_io_fail = 15, ++ .dev_loss = MAX_DEV_LOSS_TMO, ++ .detect_prio = DETECT_PRIO_OFF, + }, + /* + * Kaminario diff --git a/0081-multipathd-make-multipathd-show-status-busy-checker-.patch b/0081-multipathd-make-multipathd-show-status-busy-checker-.patch new file mode 100644 index 0000000..8bdfa10 --- /dev/null +++ b/0081-multipathd-make-multipathd-show-status-busy-checker-.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Benjamin Marzinski +Date: Wed, 21 Jan 2026 16:03:12 -0500 +Subject: [PATCH] multipathd: make "multipathd show status" busy checker better + +while uevent_listen() was grabbing new uevents, "multipathd show status" +would still show show busy as "False". Add a check there, to make catch +multipathd's uevent processing earlier. Also, access servicing_uev (as +well as the new variable, adding_uev) atomically, just to make sure that +the compiler doesn't do stupid things trying to optimize them. + +Signed-off-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +--- + libmultipath/uevent.c | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/libmultipath/uevent.c b/libmultipath/uevent.c +index 32135d7e..7cebf4f3 100644 +--- a/libmultipath/uevent.c ++++ b/libmultipath/uevent.c +@@ -64,6 +64,7 @@ static pthread_cond_t *uev_condp = &uev_cond; + static uev_trigger *my_uev_trigger; + static void *my_trigger_data; + static int servicing_uev; ++static int adding_uev; /* uatomic access only */ + + struct uevent_filter_state { + struct list_head uevq; +@@ -82,13 +83,14 @@ static void reset_filter_state(struct uevent_filter_state *st) + + int is_uevent_busy(void) + { +- int empty, servicing; ++ int empty, servicing, adding; + + pthread_mutex_lock(uevq_lockp); + empty = list_empty(&uevq); + servicing = servicing_uev; ++ adding = uatomic_read(&adding_uev); + pthread_mutex_unlock(uevq_lockp); +- return (!empty || servicing); ++ return (!empty || servicing || adding); + } + + struct uevent * alloc_uevent (void) +@@ -742,6 +744,7 @@ int uevent_listen(struct udev *udev) + int fdcount, events; + struct pollfd ev_poll = { .fd = fd, .events = POLLIN, }; + ++ uatomic_set(&adding_uev, 0); + fdcount = poll(&ev_poll, 1, -1); + if (fdcount < 0) { + if (errno == EINTR) +@@ -751,6 +754,8 @@ int uevent_listen(struct udev *udev) + err = -errno; + break; + } ++ uatomic_set(&adding_uev, 1); ++ + events = uevent_receive_events(fd, &uevlisten_tmp, monitor); + if (events <= 0) + continue; diff --git a/0082-multipathd-print-path-offline-message-even-without-a.patch b/0082-multipathd-print-path-offline-message-even-without-a.patch new file mode 100644 index 0000000..5b385af --- /dev/null +++ b/0082-multipathd-print-path-offline-message-even-without-a.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Benjamin Marzinski +Date: Wed, 21 Jan 2026 16:03:13 -0500 +Subject: [PATCH] multipathd: print path offline message even without a checker + +If a path has a checker selected and is offline, multipathd will print a +"path offline" message. However if the checker isn't selected, for +instance because multipathd was started or reconfigured while the path +was offline, multipathd was not printing the "path offline" message. +Fix that. + +Signed-off-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +--- + multipathd/main.c | 5 ++--- + 1 file changed, 2 insertions(+), 3 deletions(-) + +diff --git a/multipathd/main.c b/multipathd/main.c +index 0882ef2a..e9165350 100644 +--- a/multipathd/main.c ++++ b/multipathd/main.c +@@ -95,12 +95,11 @@ mpath_pr_event_handle(struct path *pp, unsigned int nr_keys_needed, + + #define LOG_MSG(lvl, pp) \ + do { \ +- if (pp->mpp && checker_selected(&pp->checker) && \ +- lvl <= libmp_verbosity) { \ ++ if (pp->mpp && lvl <= libmp_verbosity) { \ + if (pp->offline) \ + condlog(lvl, "%s: %s - path offline", \ + pp->mpp->alias, pp->dev); \ +- else { \ ++ else if (checker_selected(&pp->checker)) { \ + const char *__m = \ + checker_message(&pp->checker); \ + \ diff --git a/0083-libmultipath-add-purge_disconnected-configuration-op.patch b/0083-libmultipath-add-purge_disconnected-configuration-op.patch new file mode 100644 index 0000000..c3bb058 --- /dev/null +++ b/0083-libmultipath-add-purge_disconnected-configuration-op.patch @@ -0,0 +1,279 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Brian Bunker +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 +Reviewed-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +Signed-off-by: Benjamin Marzinski +--- + 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 diff --git a/0084-multipathd-implement-purge-functionality-for-disconn.patch b/0084-multipathd-implement-purge-functionality-for-disconn.patch new file mode 100644 index 0000000..83fdb09 --- /dev/null +++ b/0084-multipathd-implement-purge-functionality-for-disconn.patch @@ -0,0 +1,752 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Brian Bunker +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 +Signed-off-by: Krishna Kant +Reviewed-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +Signed-off-by: Benjamin Marzinski +--- + 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 ++ * Copyright (C) 2025 Krishna Kant ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#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 ++ * Copyright (C) 2025 Krishna Kant ++ */ ++ ++#ifndef PURGE_H_INCLUDED ++#define PURGE_H_INCLUDED ++ ++#include ++#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 */ diff --git a/0085-libmpathpersist-fix-register-retry-status-checking.patch b/0085-libmpathpersist-fix-register-retry-status-checking.patch new file mode 100644 index 0000000..bf82fcb --- /dev/null +++ b/0085-libmpathpersist-fix-register-retry-status-checking.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Benjamin Marzinski +Date: Mon, 1 Dec 2025 22:02:10 -0500 +Subject: [PATCH] libmpathpersist: fix register retry status checking + +If there libmpathpersist failed to create a thread to retry the register +and ignore command, mpath_prout_reg should fail. Instead, the code was +simply ignoring the failed threads. Fix that. + +Fixes: 2a4ca250 ("libmpathpersist: change how reservation conflicts are handled") +Signed-off-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +--- + libmpathpersist/mpath_persist_int.c | 14 +++++++------- + 1 file changed, 7 insertions(+), 7 deletions(-) + +diff --git a/libmpathpersist/mpath_persist_int.c b/libmpathpersist/mpath_persist_int.c +index 00334a1a..3f331294 100644 +--- a/libmpathpersist/mpath_persist_int.c ++++ b/libmpathpersist/mpath_persist_int.c +@@ -542,19 +542,19 @@ static int mpath_prout_reg(struct multipath *mpp,int rq_servact, int rq_scope, + } + } + for (i = 0; i < count; i++) { +- if (thread[i].param.status != MPATH_PR_SKIP && +- thread[i].param.status != MPATH_PR_THREAD_ERROR) { ++ if (thread[i].param.status == MPATH_PR_SKIP) ++ continue; ++ if (thread[i].param.status != MPATH_PR_THREAD_ERROR) { + rc = pthread_join(thread[i].id, NULL); + if (rc) { + condlog(3, "%s: failed to join thread while retrying %d", + mpp->wwid, i); + } +- if (thread[i].param.status == +- MPATH_PR_RETRYABLE_ERROR) +- retryable_error = true; +- else if (status == MPATH_PR_SUCCESS) +- status = thread[i].param.status; + } ++ if (thread[i].param.status == MPATH_PR_RETRYABLE_ERROR) ++ retryable_error = true; ++ else if (status == MPATH_PR_SUCCESS) ++ status = thread[i].param.status; + } + need_retry = false; + } diff --git a/0086-multipathd-remember-number-of-registered-keys-when-i.patch b/0086-multipathd-remember-number-of-registered-keys-when-i.patch new file mode 100644 index 0000000..60cdf1d --- /dev/null +++ b/0086-multipathd-remember-number-of-registered-keys-when-i.patch @@ -0,0 +1,59 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Benjamin Marzinski +Date: Mon, 1 Dec 2025 22:02:12 -0500 +Subject: [PATCH] multipathd: remember number of registered keys when ioctl + fails + +If prin_do_scsi_ioctl() fails in update_map_pr() for some reason other +than Persistent Reservations not being supported, It shouldn't clear the +number of registered keys, since there's no reason to think that it has +changed. Similarly, if update_map_pr() fails in mpath_pr_event_handle(), +don't assume that the nr_keys_needed was cleared. Just return whatever +the value is now. This saves multipathd from doing pointless calls to +update_map_pr(), if one of the paths is failing. + +Signed-off-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +--- + multipathd/main.c | 11 +++++++---- + 1 file changed, 7 insertions(+), 4 deletions(-) + +diff --git a/multipathd/main.c b/multipathd/main.c +index 7cb57e3c..46600527 100644 +--- a/multipathd/main.c ++++ b/multipathd/main.c +@@ -4059,7 +4059,9 @@ void unset_pr(struct multipath *mpp) + * The number of found keys must be at least as large as *nr_keys, + * and if MPATH_PR_SUCCESS is returned and mpp->prflag is PR_SET after + * the call, *nr_keys will be set to the number of found keys. Otherwise +- * it will be set to 0. ++ * if mpp->prflag is PR_UNSET it will be set to 0. If MPATH_PR_SUCCESS ++ * is not returned and mpp->prflag is not PR_UNSET, nr_keys will not be ++ * changed. + */ + static int update_map_pr(struct multipath *mpp, struct path *pp, unsigned int *nr_keys) + { +@@ -4088,11 +4090,12 @@ static int update_map_pr(struct multipath *mpp, struct path *pp, unsigned int *n + + ret = prin_do_scsi_ioctl(pp->dev, MPATH_PRIN_RKEY_SA, &resp, 0); + if (ret != MPATH_PR_SUCCESS) { +- if (ret == MPATH_PR_ILLEGAL_REQ) ++ if (ret == MPATH_PR_ILLEGAL_REQ) { + unset_pr(mpp); ++ *nr_keys = 0; ++ } + condlog(0, "%s : pr in read keys service action failed Error=%d", + mpp->alias, ret); +- *nr_keys = 0; + return ret; + } + +@@ -4208,7 +4211,7 @@ retry: + clear_reg ? "Clearing" : "Setting", pp->dev, ret); + } else if (!clear_reg) { + if (update_map_pr(mpp, pp, &nr_keys_needed) != MPATH_PR_SUCCESS) +- return 0; ++ return nr_keys_needed; + if (mpp->prflag != PR_SET) { + memset(¶m, 0, sizeof(param)); + clear_reg = true; diff --git a/0087-libmpathpersist-fix-code-for-skipping-multipathd-pat.patch b/0087-libmpathpersist-fix-code-for-skipping-multipathd-pat.patch new file mode 100644 index 0000000..437f702 --- /dev/null +++ b/0087-libmpathpersist-fix-code-for-skipping-multipathd-pat.patch @@ -0,0 +1,461 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Benjamin Marzinski +Date: Mon, 15 Dec 2025 15:29:58 -0500 +Subject: [PATCH] libmpathpersist: fix code for skipping multipathd path + registration + +When libmpathpersist notifies multipathd that a key has been registered, +cli_setprstatus() calls pr_register_active_paths() with a flag to let it +know that the paths are likely already registered, and it can skip +re-registering them, as long as the number of active paths matches the +number of registered keys. This shortcut can fail, causing multipathd to +not register needed paths, if either a path becomes usable and another +becomes unusable while libmpathpersist is running or if there already +were registered keys for I_T Nexus's that don't correspond to path +devices. + +To make this shortcut work in cases like that, this commit adds a new +multipathd command "setprstatus map pathlist ", where + is a quoted, whitespace separated list of scsi path devices. +libmpathpersist will send out the list of paths it registered the key +on. pr_register_active_paths() will skip calling mpath_pr_event_handle() +for paths on that list. + +In order to deal with the possiblity of a preempt occuring while +libmpathpersist was running, the code still needs to check that it has +the expected number of keys. + +Fixes: f7d6cd17 ("multipathd: Fix race while registering PR key") +Signed-off-by: Benjamin Marzinski +Reviewed-by: Martin Wilck +--- + .github/actions/spelling/expect.txt | 1 + + libmpathpersist/mpath_persist_int.c | 6 +-- + libmpathpersist/mpath_updatepr.c | 49 +++++++++++++++++------ + libmpathpersist/mpathpr.h | 4 +- + multipathd/callbacks.c | 2 + + multipathd/cli.c | 1 + + multipathd/cli.h | 2 + + multipathd/cli_handlers.c | 35 ++++++++++++++-- + multipathd/main.c | 62 +++++++++++++++++++---------- + multipathd/main.h | 4 +- + multipathd/multipathd.8.in | 10 ++++- + 11 files changed, 132 insertions(+), 44 deletions(-) + +diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt +index e2d4acf9..9bb8f8dd 100644 +--- a/.github/actions/spelling/expect.txt ++++ b/.github/actions/spelling/expect.txt +@@ -143,6 +143,7 @@ OPTFLAGS + paramp + partx + pathgroup ++pathlist + petabytes + pgpolicy + plugindir +diff --git a/libmpathpersist/mpath_persist_int.c b/libmpathpersist/mpath_persist_int.c +index 3f331294..b2c3e27a 100644 +--- a/libmpathpersist/mpath_persist_int.c ++++ b/libmpathpersist/mpath_persist_int.c +@@ -1018,12 +1018,12 @@ int do_mpath_persistent_reserve_out(vector curmp, vector pathvec, int fd, + case MPATH_PROUT_REG_SA: + case MPATH_PROUT_REG_IGN_SA: + if (unregistering) +- update_prflag(alias, 0); ++ update_prflag(mpp, 0); + else +- update_prflag(alias, 1); ++ update_prflag(mpp, 1); + break; + case MPATH_PROUT_CLEAR_SA: +- update_prflag(alias, 0); ++ update_prflag(mpp, 0); + if (mpp->prkey_source == PRKEY_SOURCE_FILE) + update_prkey(alias, 0); + break; +diff --git a/libmpathpersist/mpath_updatepr.c b/libmpathpersist/mpath_updatepr.c +index dd8dd48e..f943b1bf 100644 +--- a/libmpathpersist/mpath_updatepr.c ++++ b/libmpathpersist/mpath_updatepr.c +@@ -20,8 +20,9 @@ + #include "uxsock.h" + #include "mpathpr.h" + #include "structs.h" ++#include "strbuf.h" + +-static char *do_pr(char *alias, char *str) ++static char *do_pr(char *alias, const char *str) + { + int fd; + char *reply; +@@ -51,24 +52,26 @@ static char *do_pr(char *alias, char *str) + return reply; + } + +-static int do_update_pr(char *alias, char *cmd, char *key) ++static int do_update_pr(char *alias, char *cmd, const char *data) + { +- char str[256]; ++ STRBUF_ON_STACK(buf); + char *reply = NULL; + int ret = -1; + +- if (key) +- snprintf(str, sizeof(str), "%s map %s key %s", cmd, alias, key); ++ if (data) ++ print_strbuf(&buf, "%s map %s %s %s", cmd, alias, ++ strcmp(cmd, "setprkey") ? "pathlist" : "key", data); + else +- snprintf(str, sizeof(str), "%s map %s", cmd, alias); ++ print_strbuf(&buf, "%s map %s", cmd, alias); + +- reply = do_pr(alias, str); ++ reply = do_pr(alias, get_strbuf_str(&buf)); + if (reply) { +- condlog (2, "%s: message=%s reply=%s", alias, str, reply); +- if (reply && strncmp(reply,"ok", 2) == 0) ++ if (strncmp(reply, "ok", 2) == 0) + ret = 0; + else + ret = -1; ++ condlog(ret ? 0 : 4, "%s: message=%s reply=%s", alias, ++ get_strbuf_str(&buf), reply); + } + + free(reply); +@@ -106,9 +109,31 @@ int get_prhold(char *mapname) + return do_get_pr(mapname, "getprhold"); + } + +-int update_prflag(char *mapname, int set) { +- return do_update_pr(mapname, (set)? "setprstatus" : "unsetprstatus", +- NULL); ++int update_prflag(struct multipath *mpp, int set) ++{ ++ STRBUF_ON_STACK(buf); ++ int i, j; ++ bool first = true; ++ struct pathgroup *pgp = NULL; ++ struct path *pp = NULL; ++ ++ if (!set) ++ return do_update_pr(mpp->alias, "unsetprstatus", NULL); ++ ++ append_strbuf_str(&buf, "\""); ++ vector_foreach_slot (mpp->pg, pgp, j) { ++ vector_foreach_slot (pgp->paths, pp, i) { ++ if (pp->state == PATH_UP || pp->state == PATH_GHOST) { ++ if (first) { ++ append_strbuf_str(&buf, pp->dev); ++ first = false; ++ } else ++ print_strbuf(&buf, " %s", pp->dev_t); ++ } ++ } ++ } ++ append_strbuf_str(&buf, "\""); ++ return do_update_pr(mpp->alias, "setprstatus", get_strbuf_str(&buf)); + } + + int update_prhold(char *mapname, bool set) +diff --git a/libmpathpersist/mpathpr.h b/libmpathpersist/mpathpr.h +index b668ef58..baee9a59 100644 +--- a/libmpathpersist/mpathpr.h ++++ b/libmpathpersist/mpathpr.h +@@ -1,12 +1,14 @@ + #ifndef MPATHPR_H + #define MPATHPR_H + ++#include "structs.h" ++ + /* + * This header file contains symbols that are only used by + * libmpathpersist internally. + */ + +-int update_prflag(char *mapname, int set); ++int update_prflag(struct multipath *mpp, int set); + int update_prkey_flags(char *mapname, uint64_t prkey, uint8_t sa_flags); + int get_prflag(char *mapname); + int get_prhold(char *mapname); +diff --git a/multipathd/callbacks.c b/multipathd/callbacks.c +index b6b57f45..2027c3a6 100644 +--- a/multipathd/callbacks.c ++++ b/multipathd/callbacks.c +@@ -59,6 +59,8 @@ void init_handler_callbacks(void) + set_unlocked_handler_callback(VRB_SHUTDOWN, HANDLER(cli_shutdown)); + set_handler_callback(VRB_GETPRSTATUS | Q1_MAP, HANDLER(cli_getprstatus)); + set_handler_callback(VRB_SETPRSTATUS | Q1_MAP, HANDLER(cli_setprstatus)); ++ set_handler_callback(VRB_SETPRSTATUS | Q1_MAP | Q2_PATHLIST, ++ HANDLER(cli_setprstatus_list)); + set_handler_callback(VRB_UNSETPRSTATUS | Q1_MAP, HANDLER(cli_unsetprstatus)); + set_handler_callback(VRB_FORCEQ | Q1_DAEMON, HANDLER(cli_force_no_daemon_q)); + set_handler_callback(VRB_RESTOREQ | Q1_DAEMON, HANDLER(cli_restore_no_daemon_q)); +diff --git a/multipathd/cli.c b/multipathd/cli.c +index bccdda48..4ff229a5 100644 +--- a/multipathd/cli.c ++++ b/multipathd/cli.c +@@ -227,6 +227,7 @@ load_keys (void) + r += add_key(keys, "getprhold", VRB_GETPRHOLD, 0); + r += add_key(keys, "setprhold", VRB_SETPRHOLD, 0); + r += add_key(keys, "unsetprhold", VRB_UNSETPRHOLD, 0); ++ r += add_key(keys, "pathlist", KEY_PATHLIST, 1); + + if (r) { + free_keys(keys); +diff --git a/multipathd/cli.h b/multipathd/cli.h +index 925575ad..70a2d3f0 100644 +--- a/multipathd/cli.h ++++ b/multipathd/cli.h +@@ -62,6 +62,7 @@ enum { + KEY_LOCAL = 81, + KEY_GROUP = 82, + KEY_KEY = 83, ++ KEY_PATHLIST = 84, + }; + + /* +@@ -94,6 +95,7 @@ enum { + Q2_LOCAL = KEY_LOCAL << 16, + Q2_GROUP = KEY_GROUP << 16, + Q2_KEY = KEY_KEY << 16, ++ Q2_PATHLIST = KEY_PATHLIST << 16, + + /* byte 3: qualifier 3 */ + Q3_FMT = KEY_FMT << 24, +diff --git a/multipathd/cli_handlers.c b/multipathd/cli_handlers.c +index 1677dd1e..854f6192 100644 +--- a/multipathd/cli_handlers.c ++++ b/multipathd/cli_handlers.c +@@ -32,6 +32,7 @@ + #include "strbuf.h" + #include "cli_handlers.h" + #include "devmapper.h" ++#include + + static struct path * + find_path_by_str(const struct _vector *pathvec, const char *str, +@@ -1266,8 +1267,8 @@ cli_getprstatus (void * v, struct strbuf *reply, void * data) + return 0; + } + +-static int +-cli_setprstatus(void * v, struct strbuf *reply, void * data) ++static int do_setprstatus(void *v, struct strbuf *reply, void *data, ++ const struct _vector *registered_paths) + { + struct multipath * mpp; + struct vectors * vecs = (struct vectors *)data; +@@ -1281,7 +1282,7 @@ cli_setprstatus(void * v, struct strbuf *reply, void * data) + + if (mpp->prflag != PR_SET) { + set_pr(mpp); +- pr_register_active_paths(mpp, true); ++ pr_register_active_paths(mpp, registered_paths); + if (mpp->prflag == PR_SET) + condlog(2, "%s: prflag set", param); + else +@@ -1292,6 +1293,34 @@ cli_setprstatus(void * v, struct strbuf *reply, void * data) + return 0; + } + ++static int cli_setprstatus(void *v, struct strbuf *reply, void *data) ++{ ++ return do_setprstatus(v, reply, data, NULL); ++} ++ ++static int cli_setprstatus_list(void *v, struct strbuf *reply, void *data) ++{ ++ int r; ++ struct _vector registered_paths_vec = {.allocated = 0}; ++ vector registered_paths ++ __attribute__((cleanup(cleanup_reset_vec))) = ®istered_paths_vec; ++ char *ptr = get_keyparam(v, KEY_PATHLIST); ++ ++ while (isspace(*ptr)) ++ ptr++; ++ while (*ptr) { ++ if (!vector_alloc_slot(registered_paths)) ++ return -ENOMEM; ++ vector_set_slot(registered_paths, ptr); ++ while (*ptr && !isspace(*ptr)) ++ ptr++; ++ while (isspace(*ptr)) ++ *ptr++ = '\0'; ++ } ++ r = do_setprstatus(v, reply, data, registered_paths); ++ return r; ++} ++ + static int + cli_unsetprstatus(void * v, struct strbuf *reply, void * data) + { +diff --git a/multipathd/main.c b/multipathd/main.c +index 46600527..bdb04a1f 100644 +--- a/multipathd/main.c ++++ b/multipathd/main.c +@@ -637,28 +637,47 @@ flush_map_nopaths(struct multipath *mpp, struct vectors *vecs) { + return true; + } + +-void pr_register_active_paths(struct multipath *mpp, bool check_nr_active) ++/* ++ * If reg_paths in non-NULL, it is a vector of paths that libmpathpersist ++ * registered. If the number of registered keys is smaller than the number ++ * of registered paths, then likely a preempt that occurred while ++ * libmpathpersist was registering the key. As long as there are still some ++ * registered keys, treat the preempt as happening first, and make sure to ++ * register keys on all the paths. If the number of registered keys is at ++ * least as large as the number of registered paths, then no preempt happened, ++ * and multipathd does not need to re-register the paths that libmpathpersist ++ * handled ++ */ ++void pr_register_active_paths(struct multipath *mpp, const struct _vector *reg_paths) + { +- unsigned int i, j, nr_keys = 0; +- unsigned int nr_active = 0; ++ unsigned int i, j, k, nr_keys = 0; ++ unsigned int wanted_nr = VECTOR_SIZE(reg_paths); + struct path *pp; + struct pathgroup *pgp; +- +- if (check_nr_active) { +- nr_active = count_active_paths(mpp); +- if (!nr_active) +- return; +- } ++ char *pathname; + + vector_foreach_slot (mpp->pg, pgp, i) { + vector_foreach_slot (pgp->paths, pp, j) { + if (mpp->prflag == PR_UNSET) + return; +- if (pp->state == PATH_UP || pp->state == PATH_GHOST) { +- nr_keys = mpath_pr_event_handle(pp, nr_keys, nr_active); +- if (check_nr_active && nr_keys == nr_active) +- return; ++ if (pp->state != PATH_UP && pp->state != PATH_GHOST) ++ continue; ++ if (wanted_nr && nr_keys) { ++ vector_foreach_slot (reg_paths, pathname, k) { ++ if (strcmp(pp->dev_t, pathname) == 0) { ++ goto skip; ++ } ++ } + } ++ nr_keys = mpath_pr_event_handle(pp, nr_keys, wanted_nr); ++ if (nr_keys && nr_keys < wanted_nr) { ++ /* ++ * Incorrect number of registered keys. Need ++ * to register all devices ++ */ ++ wanted_nr = 0; ++ } ++ skip:; /* a statement must follow a label on pre C23 clang */ + } + } + } +@@ -688,8 +707,7 @@ handle_orphaned_offline_paths(vector offline_paths) + pp->add_when_online = true; + } + +-static void +-cleanup_reset_vec(struct _vector **v) ++void cleanup_reset_vec(struct _vector **v) + { + vector_reset(*v); + } +@@ -745,7 +763,7 @@ fail: + + sync_map_state(mpp); + +- pr_register_active_paths(mpp, false); ++ pr_register_active_paths(mpp, NULL); + + if (VECTOR_SIZE(offline_paths) != 0) + handle_orphaned_offline_paths(offline_paths); +@@ -1393,7 +1411,7 @@ rescan: + + if (retries >= 0) { + if ((mpp->prflag == PR_SET && prflag != PR_SET) || start_waiter) +- pr_register_active_paths(mpp, false); ++ pr_register_active_paths(mpp, NULL); + condlog(2, "%s [%s]: path added to devmap %s", + pp->dev, pp->dev_t, mpp->alias); + return 0; +@@ -2688,7 +2706,7 @@ check_path (struct vectors * vecs, struct path * pp, unsigned int ticks) + mpath_pr_event_handle(pp, 0, 0); + if (pp->mpp->prflag == PR_SET && + prflag != PR_SET) +- pr_register_active_paths(pp->mpp, false); ++ pr_register_active_paths(pp->mpp, NULL); + } + } + +@@ -3091,7 +3109,7 @@ configure (struct vectors * vecs, enum force_reload_types reload_type) + vector_foreach_slot(mpvec, mpp, i){ + if (remember_wwid(mpp->wwid) == 1) + trigger_paths_udev_change(mpp, true); +- pr_register_active_paths(mpp, false); ++ pr_register_active_paths(mpp, NULL); + } + + /* +@@ -4151,8 +4169,8 @@ static int update_map_pr(struct multipath *mpp, struct path *pp, unsigned int *n + * + * nr_keys_wanted: Only used if nr_keys_needed is 0, so we don't know how + * many keys we currently have. If nr_keys_wanted in non-zero and the +- * number of keys found by the initial call to update_map_pr() matches it, +- * exit early, since we have all the keys we are expecting. ++ * number of keys found by the initial call to update_map_pr() is at least ++ * as large as it, exit early, since we have all the keys we are expecting. + * + * The function returns the number of keys that are registered or 0 if + * it's unknown. +@@ -4175,7 +4193,7 @@ mpath_pr_event_handle(struct path *pp, unsigned int nr_keys_needed, + nr_keys_needed = 1; + if (update_map_pr(mpp, pp, &nr_keys_needed) != MPATH_PR_SUCCESS) + return 0; +- if (nr_keys_wanted && nr_keys_wanted == nr_keys_needed) ++ if (nr_keys_wanted && nr_keys_wanted <= nr_keys_needed) + return nr_keys_needed; + } + +diff --git a/multipathd/main.h b/multipathd/main.h +index 25ddbaa7..0742ca2e 100644 +--- a/multipathd/main.h ++++ b/multipathd/main.h +@@ -54,5 +54,7 @@ int resize_map(struct multipath *mpp, unsigned long long size, + struct vectors *vecs); + void set_pr(struct multipath *mpp); + void unset_pr(struct multipath *mpp); +-void pr_register_active_paths(struct multipath *mpp, bool check_active_nr); ++void pr_register_active_paths(struct multipath *mpp, ++ const struct _vector *registered_paths); ++void cleanup_reset_vec(struct _vector **v); + #endif /* MAIN_H */ +diff --git a/multipathd/multipathd.8.in b/multipathd/multipathd.8.in +index 342e363e..2420d698 100644 +--- a/multipathd/multipathd.8.in ++++ b/multipathd/multipathd.8.in +@@ -346,11 +346,17 @@ will not be disabled when the daemon stops. + Restores configured queue_without_daemon mode. + . + .TP +-.B map|multipath $map setprstatus ++.B setprstatus map|multipath $map + Enable persistent reservation management on $map. + . + .TP +-.B map|multipath $map unsetprstatus ++.B setprstatus map|multipath $map pathlist $pathlist ++Enable persistent reservation management on $map, and notify multipathd of ++the paths that have been registered, so it doesn't attempt to re-register ++them. ++. ++.TP ++.B unsetprstatus map|multipath $map + Disable persistent reservation management on $map. + . + .TP diff --git a/device-mapper-multipath.spec b/device-mapper-multipath.spec index 12d4539..08b76ac 100644 --- a/device-mapper-multipath.spec +++ b/device-mapper-multipath.spec @@ -1,6 +1,6 @@ Name: device-mapper-multipath Version: 0.9.9 -Release: 12%{?dist}.1 +Release: 18%{?dist} Summary: Tools to manage multipath devices using device-mapper License: GPLv2 URL: http://christophe.varoqui.free.fr/ @@ -89,6 +89,14 @@ Patch0076: 0076-libmpathpersist-Fix-race-between-restoring-a-path-an.patch Patch0077: 0077-multipathd-Fix-tracking-of-old-PR-key.patch Patch0078: 0078-multipathd-Fix-race-while-registering-PR-key.patch 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 +Patch0085: 0085-libmpathpersist-fix-register-retry-status-checking.patch +Patch0086: 0086-multipathd-remember-number-of-registered-keys-when-i.patch +Patch0087: 0087-libmpathpersist-fix-code-for-skipping-multipathd-pat.patch # runtime Requires: %{name}-libs = %{version}-%{release} @@ -298,7 +306,45 @@ fi %{_pkgconfdir}/libdmmp.pc %changelog -* Tue Nov 11 2025 Lin Li - 0.9.9-12.1 +* Thu Feb 19 2026 Benjamin Marzinski - 0.9.9-18 +- Add 0085-libmpathpersist-fix-register-retry-status-checking.patch +- Add 0086-multipathd-remember-number-of-registered-keys-when-i.patch +- Add 0087-libmpathpersist-fix-code-for-skipping-multipathd-pat.patch + * Fixes RHEL-129442 ("Improve multipathd's handling of updating + persistent reservations on restored paths.") +- Resolves: RHEL-129442 + +* Thu Jan 29 2026 Benjamin Marzinski - 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 - 0.9.9-16 +- Add 0081-multipathd-make-multipathd-show-status-busy-checker-.patch + * Fixes RHEL-136405 ("improve Busy checking for multipathd show status + output") +- Add 0082-multipathd-print-path-offline-message-even-without-a.patch + * Fixes RHEL-133815 ("log_checker_err is not printing messages + repeatedly for failed path [rhel-10]") +- Resolves: RHEL-133815 +- Resolves: RHEL-136405 + + +* Wed Nov 19 2025 Benjamin Marzinski - 0.9.9-15 +- Add 0080-multipath-tools-update-NFINIDAT-InfiniBox-config-in-.patch + * Fixes RHEL-128338 ("Update the multipath.conf stanza for Infinidat + storage") +- Resolves: RHEL-128338 + +* Tue Nov 11 2025 Benjamin Marzinski - 0.9.9-14 +- Add 0078-multipathd-Fix-race-while-registering-PR-key.patch +- Add 0079-mpathpersist-Fix-REPORT-CAPABILITIES-output.patch + * Fixes RHEL-118720 ("There are many bugs in multipath's persistent + reservation handling [rhel-10]") +- Resolves: RHEL-118720 + +* Wed Oct 1 2025 Benjamin Marzinski - 0.9.9-13 - Add 0035-libmpathpersist-fix-memory-leak-in-mpath_prout_rel.patch - Add 0036-libmpathpersist-retry-commands-on-other-paths-in-mpa.patch - Add 0037-libmpathpersist-check-released-key-against-the-reser.patch @@ -342,11 +388,9 @@ fi - Add 0075-libmpathpersist-Fix-unregistering-while-holding-the-.patch - Add 0076-libmpathpersist-Fix-race-between-restoring-a-path-an.patch - Add 0077-multipathd-Fix-tracking-of-old-PR-key.patch -- Add 0078-multipathd-Fix-race-while-registering-PR-key.patch -- Add 0079-mpathpersist-Fix-REPORT-CAPABILITIES-output.patch - * Fixes RHEL-125288 ("There are many bugs in multipath's persistent - reservation handling [rhel-10.1.z]") -- Resolves: RHEL-125288 + * Fixes RHEL-118720 ("There are many bugs in multipath's persistent + reservation handling [rhel-10]") +- Resolves: RHEL-118720 * Sat Sep 13 2025 Lin Li - 0.9.9-12 Add 0034-libmultipath-fix-crash-in-print_foreign_topology.patch