From: Benjamin Marzinski <bmarzins@redhat.com>
To: Brian Bunker <brian@purestorage.com>
Cc: dm-devel@lists.linux.dev, Martin Wilck <mwilck@suse.com>,
Christophe Varoqui <christophe.varoqui@opensvc.com>
Subject: Re: [PATCH v4 1/1] multipathd: don't leave maps read-only after write protect is cleared
Date: Tue, 27 Oct 2026 00:17:00 -0400 [thread overview]
Message-ID: <auAlvMxSuLghYscn@redhat.com> (raw)
In-Reply-To: <20261022234853.79725-2-brian@purestorage.com>
On Thu, Oct 22, 2026 at 04:48:53PM -0700, Brian Bunker wrote:
> ev_add_path() sets mpp->force_readonly when a newly added path is
> read-only, but never clears it. If a map is created or reloaded while
> its storage is write protected, the flag stays set. When write protect
> is cleared, the reload that uev_update_path() does for the DISK_RO=0
> uevent uses the stale flag and loads the map read-only, even though all
> of its paths are read-write. uev_update_path() clears the flag after
> that reload, so the map only recovers if another DISK_RO=0 uevent
> reloads it again. If none comes, the map stays read-only, and since
> mpp->dmi.read_only is then 1, needs_ro_update() also ignores later
> DISK_RO=1 uevents for it.
>
> Fix this by setting force_readonly before each load and clearing it
> when the load reads it:
>
> - needs_ro_update() now sets force_readonly whenever it returns true:
> to 1 for a DISK_RO=1 uevent, and to 0 when all of the map's paths
> are writable. uev_update_path() no longer sets or clears it itself.
> If the map has no dm info, needs_ro_update() now fetches it, and
> returns false if that fails, instead of assuming a reload is needed.
>
> - ev_add_path() sets it from all of the map's paths rather than only
> the path being added, and only when it is about to load the map
> (need_do_map). mpp->paths is up to date there after adopt_paths(),
> while mpp->pg doesn't contain the new paths yet.
>
> - update_map(), which does reloads that were delayed while the map was
> waiting for udev (UDEV_WAIT_RELOAD), recomputes it from the map's
> paths before setting up the map.
>
> - dm_addmap_create() and dm_addmap_reload() clear it as soon as they
> have read it, so it only applies to the load it was set for.
>
> - If a reload fails, dm_addmap_reload() sets it back, because the map
> still has its old table. If the map was supposed to become read-only,
> the next reload then tries again. needs_ro_update() clears the flag
> again when a DISK_RO=0 uevent arrives for a read-write map and none
> of the map's paths is read-only any more, so that a later reload for
> an unrelated reason, such as a path group change, doesn't load the
> map read-only.
>
> Signed-off-by: Brian Bunker <brian@purestorage.com>
> ---
> libmultipath/devmapper.c | 26 ++++++--
> multipathd/main.c | 128 +++++++++++++++++++++++++--------------
> 2 files changed, 103 insertions(+), 51 deletions(-)
>
> diff --git a/libmultipath/devmapper.c b/libmultipath/devmapper.c
> index e3870f59..324a92cc 100644
> --- a/libmultipath/devmapper.c
> +++ b/libmultipath/devmapper.c
> @@ -539,10 +539,15 @@ static uint16_t build_udev_flags(const struct multipath *mpp, int reload)
>
> int dm_addmap_create (struct multipath *mpp, char *params)
> {
> - int ro;
> + int ro = mpp->force_readonly ? 1 : 0;
> uint16_t udev_flags = build_udev_flags(mpp, 0);
>
> - for (ro = mpp->force_readonly ? 1 : 0; ro <= 1; ro++) {
> + /*
> + * force_readonly only applies to this load. Clear it, so that it
> + * can't affect later loads, even if this one fails.
> + */
> + mpp->force_readonly = 0;
> + for (; ro <= 1; ro++) {
> int err;
>
> if (dm_addmap(DM_DEVICE_CREATE, TGT_MPATH, mpp, params, ro,
> @@ -578,19 +583,26 @@ int dm_addmap_create (struct multipath *mpp, char *params)
> int dm_addmap_reload(struct multipath *mpp, char *params, int flush)
> {
> int r = 0;
> + int force_ro = mpp->force_readonly;
> uint16_t udev_flags = build_udev_flags(mpp, 1);
>
> + /*
> + * force_readonly only applies to this load. Clear it, so that it
> + * can't affect later loads, unless this one fails (see below).
> + */
> + mpp->force_readonly = 0;
> +
> /*
> * DM_DEVICE_RELOAD cannot wait on a cookie, as
> * the cookie will only ever be released after an
> * DM_DEVICE_RESUME. So call DM_DEVICE_RESUME
> * after each successful call to DM_DEVICE_RELOAD.
> */
> - if (!mpp->force_readonly)
> + if (!force_ro)
> r = dm_addmap(DM_DEVICE_RELOAD, TGT_MPATH, mpp, params,
> ADDMAP_RW, 0);
> if (!r) {
> - if (!mpp->force_readonly && errno != EROFS)
> + if (!force_ro && errno != EROFS)
> return 0;
> r = dm_addmap(DM_DEVICE_RELOAD, TGT_MPATH, mpp,
> params, ADDMAP_RO, 0);
> @@ -609,6 +621,12 @@ int dm_addmap_reload(struct multipath *mpp, char *params, int flush)
> dm_simplecmd(DM_DEVICE_RESUME, mpp->alias,
> DMFL_NEED_SYNC | (flush ? 0 : DMFL_NO_FLUSH),
> udev_flags);
> + /*
> + * The map still has its old table. If it was supposed to become
> + * read-only, keep force_readonly set, so that the next reload
> + * tries again.
> + */
> + mpp->force_readonly = force_ro;
> return 0;
Unsetting and then resetting force_readonly seems like more work than
necessary. It seems simpler to just clear mpp->force_readonly if we
successfully reload the table and resume the map.
> }
>
> diff --git a/multipathd/main.c b/multipathd/main.c
> index 9bce4d6b..ddef64fa 100644
> --- a/multipathd/main.c
> +++ b/multipathd/main.c
> @@ -709,6 +709,55 @@ void cleanup_reset_vec(struct vector_s **v)
> vector_reset(*v);
> }
>
> +static int
> +sysfs_get_ro (struct path *pp)
> +{
> + int ro;
> + char buff[3]; /* Either "0\n\0" or "1\n\0" */
> +
> + if (!pp->udev)
> + return -1;
> +
> + if (!sysfs_attr_get_value_ok(pp->udev, "ro", buff, sizeof(buff))) {
> + condlog(3, "%s: Cannot read ro attribute in sysfs", pp->dev);
> + return -1;
> + }
> +
> + if (sscanf(buff, "%d\n", &ro) != 1 || ro < 0 || ro > 1) {
> + condlog(3, "%s: Cannot parse ro attribute", pp->dev);
> + return -1;
> + }
> +
> + return ro;
> +}
> +
> +/* A map must be loaded read-only if any of its paths is read-only. */
> +static bool
> +has_ro_path(vector paths)
> +{
> + struct path *pp;
> + unsigned int i;
> +
> + vector_foreach_slot (paths, pp, i) {
> + if (sysfs_get_ro(pp) == 1)
> + return true;
> + }
> + return false;
> +}
> +
> +static bool
> +map_has_ro_path(struct multipath *mpp)
> +{
> + struct pathgroup *pgp;
> + unsigned int i;
> +
> + vector_foreach_slot (mpp->pg, pgp, i) {
> + if (has_ro_path(pgp->paths))
> + return true;
> + }
> + return false;
> +}
> +
> static int
> update_map (struct multipath *mpp, struct vectors *vecs, int new_map)
> {
> @@ -732,6 +781,14 @@ retry:
>
> mpp->action = ACT_RELOAD;
>
> + /*
> + * This may be a delayed reload, and the paths' write protect
> + * state may have changed since force_readonly was last set.
> + * Recompute it from the current paths. mpp->paths is up to date
> + * after adopt_paths(), while mpp->pg isn't until setup_map().
> + */
> + mpp->force_readonly = has_ro_path(mpp->paths);
> +
> if (setup_map(mpp, ¶ms, vecs)) {
> condlog(0, "%s: failed to setup new map in update", mpp->alias);
> retries = -1;
> @@ -1270,28 +1327,6 @@ out:
> return ret;
> }
>
> -static int
> -sysfs_get_ro (struct path *pp)
> -{
> - int ro;
> - char buff[3]; /* Either "0\n\0" or "1\n\0" */
> -
> - if (!pp->udev)
> - return -1;
> -
> - if (!sysfs_attr_get_value_ok(pp->udev, "ro", buff, sizeof(buff))) {
> - condlog(3, "%s: Cannot read ro attribute in sysfs", pp->dev);
> - return -1;
> - }
> -
> - if (sscanf(buff, "%d\n", &ro) != 1 || ro < 0 || ro > 1) {
> - condlog(3, "%s: Cannot parse ro attribute", pp->dev);
> - return -1;
> - }
> -
> - return ro;
> -}
> -
> /*
> * returns:
> * 0: added
> @@ -1305,7 +1340,6 @@ ev_add_path (struct path * pp, struct vectors * vecs, int need_do_map)
> int retries = 3;
> int start_waiter = 0;
> int ret;
> - int ro;
> unsigned char prflag = PR_UNSET;
>
> /*
> @@ -1370,14 +1404,16 @@ rescan:
> goto fail; /* leave path added to pathvec */
> }
>
> - /* ro check - if new path is ro, force map to be ro as well */
> - ro = sysfs_get_ro(pp);
> - if (ro == 1)
> - mpp->force_readonly = 1;
> -
> if (!need_do_map)
> return 0;
>
> + /*
> + * Load the map read-only if any of its paths is read-only, not
> + * just the one being added. mpp->paths is up to date after
> + * adopt_paths(), while mpp->pg doesn't have the new paths yet.
> + */
> + mpp->force_readonly = has_ro_path(mpp->paths);
> +
> if (!dm_map_present(mpp->alias)) {
> mpp->action = ACT_CREATE;
> start_waiter = 1;
> @@ -1596,24 +1632,26 @@ finish_path_init(struct path *pp, struct vectors * vecs)
> static bool
> needs_ro_update(struct multipath *mpp, int ro)
> {
> - struct pathgroup * pgp;
> - struct path * pp;
> - unsigned int i, j;
> -
> if (!mpp || ro < 0)
> return false;
I said we should clear mpp->force_reload when we return false, but we
obviously can't do that when mpp is NULL. Perhaps if ro is -1 in
uev_update_path(), we should check sysfs instead. Something like:
if (ro < 0)
ro = (sysfs_get_ro == 1);
That way we'd always call needs_ro_update() with a valid value for
ro.
> - if (!has_dm_info(mpp))
> - return true;
> - if (mpp->dmi.read_only == ro)
> + if (!has_dm_info(mpp) &&
> + dm_get_info(mpp->alias, &mpp->dmi) != DMP_OK) {
> + condlog(2, "%s: failed to get dm info", mpp->alias);
> return false;
Not sure it matters what we do here. I supposed that we could check all
the paths and set force_readonly if one of them is read-only.
> - if (ro == 1)
> - return true;
> - vector_foreach_slot (mpp->pg, pgp, i) {
> - vector_foreach_slot (pgp->paths, pp, j) {
> - if (sysfs_get_ro(pp) == 1)
> - return false;
> - }
> }
> + if (mpp->dmi.read_only == ro) {
> + /*
> + * A failed read-only reload leaves force_readonly set while
> + * the map is still read-write. Clear it once no path is
> + * read-only any more.
> + */
> + if (ro == 0 && mpp->force_readonly && !map_has_ro_path(mpp))
> + mpp->force_readonly = 0;
This is correct.
> + return false;
> + }
> + if (ro == 0 && map_has_ro_path(mpp))
If ro is 0 and not equal to mpp->dmi.read_only, then that means the
map is already read-only. We don't need force_readonly here, since
dm_addmap_reload() won't be able to reload the map read-write unless it
can actually re-open the devices read-write (the path devices in the
existing table must be opened read-only since the multipath device is
read-only, so dm can't simply reuse them for the new table that it's
loading).
-Ben
> + return false;
> + mpp->force_readonly = ro;
> return true;
> }
>
> @@ -1732,15 +1770,11 @@ uev_update_path (struct uevent *uev, struct vectors * vecs)
> if (mpp->wait_for_udev != UDEV_WAIT_DONE)
> mpp->wait_for_udev = UDEV_WAIT_RELOAD;
> else {
> - if (ro == 1)
> - pp->mpp->force_readonly = 1;
> retval = reload_and_sync_map(mpp, vecs);
> if (retval == 2)
> condlog(2, "%s: map removed during reload", pp->dev);
> - else {
> - pp->mpp->force_readonly = 0;
> + else
> condlog(2, "%s: map %s reloaded (retval %d)", uev->kernel, mpp->alias, retval);
> - }
> }
> }
> if (auto_resize != AUTO_RESIZE_NEVER && mpp &&
> --
> 2.56.0
prev parent reply other threads:[~2026-10-27 4:17 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 23:48 [PATCH v4 0/1] multipathd: don't leave maps read-only after write protect is cleared Brian Bunker
2026-10-22 23:48 ` [PATCH v4 1/1] " Brian Bunker
2026-10-27 4:17 ` Benjamin Marzinski [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=auAlvMxSuLghYscn@redhat.com \
--to=bmarzins@redhat.com \
--cc=brian@purestorage.com \
--cc=christophe.varoqui@opensvc.com \
--cc=dm-devel@lists.linux.dev \
--cc=mwilck@suse.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.