* [PATCH v2 1/4] scsi: core: Move REPORT TARGET PORT GROUPS into the SCSI core
2026-10-28 23:08 [PATCH v2 0/4] scsi: Bind sd to LUs in the ALUA unavailable state Brian Bunker
@ 2026-10-28 23:08 ` Brian Bunker
2026-10-28 23:08 ` [PATCH v2 2/4] scsi: core: Read the ALUA access state when a device is scanned Brian Bunker
` (2 subsequent siblings)
3 siblings, 0 replies; 6+ messages in thread
From: Brian Bunker @ 2026-10-28 23:08 UTC (permalink / raw)
To: linux-scsi
Cc: Martin K . Petersen, James E . J . Bottomley, Hannes Reinecke,
Bart Van Assche, Krishna Kant
ALUA is a standard SCSI feature, independent of multipathing, but only
the ALUA device handler can currently send REPORT TARGET PORT GROUPS.
Move submit_rtpg() into the SCSI core as scsi_report_tpgs(), together
with the resubmission with a larger buffer that alua_rtpg() did when
the parameter data did not fit, so that the core can read the ALUA
state of a device itself. The parameter data format definitions move to
scsi_proto.h with it.
scsi_dh_alua calls scsi_report_tpgs() with the same timeout, retries
and parameter data format as before.
No functional change.
Suggested-by: Hannes Reinecke <hare@kernel.org>
Co-developed-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Brian Bunker <brian@purestorage.com>
---
drivers/scsi/device_handler/scsi_dh_alua.c | 63 ++++++----------------
drivers/scsi/scsi_lib.c | 52 ++++++++++++++++++
include/scsi/scsi_device.h | 3 ++
include/scsi/scsi_proto.h | 3 ++
4 files changed, 73 insertions(+), 48 deletions(-)
diff --git a/drivers/scsi/device_handler/scsi_dh_alua.c b/drivers/scsi/device_handler/scsi_dh_alua.c
index 87ac0a6d63e5..06eb35cc9d7c 100644
--- a/drivers/scsi/device_handler/scsi_dh_alua.c
+++ b/drivers/scsi/device_handler/scsi_dh_alua.c
@@ -29,9 +29,6 @@
#define TPGS_SUPPORT_TRANSITION 0x80
#define TPGS_SUPPORT_ALL 0xdf
-#define RTPG_FMT_MASK 0x70
-#define RTPG_FMT_EXT_HDR 0x10
-
#define TPGS_MODE_UNINITIALIZED -1
#define TPGS_MODE_NONE 0x0
#define TPGS_MODE_IMPLICIT 0x1
@@ -120,34 +117,6 @@ static void release_port_group(struct kref *kref)
kfree_rcu(pg, rcu);
}
-/*
- * submit_rtpg - Issue a REPORT TARGET GROUP STATES command
- * @sdev: sdev the command should be sent to
- */
-static int submit_rtpg(struct scsi_device *sdev, unsigned char *buff,
- int bufflen, struct scsi_sense_hdr *sshdr, int flags)
-{
- u8 cdb[MAX_COMMAND_SIZE];
- blk_opf_t opf = REQ_OP_DRV_IN | REQ_FAILFAST_DEV |
- REQ_FAILFAST_TRANSPORT | REQ_FAILFAST_DRIVER;
- const struct scsi_exec_args exec_args = {
- .sshdr = sshdr,
- };
-
- /* Prepare the command. */
- memset(cdb, 0x0, MAX_COMMAND_SIZE);
- cdb[0] = MAINTENANCE_IN;
- if (!(flags & ALUA_RTPG_EXT_HDR_UNSUPP))
- cdb[1] = MI_REPORT_TARGET_PGS | MI_EXT_HDR_PARAM_FMT;
- else
- cdb[1] = MI_REPORT_TARGET_PGS;
- put_unaligned_be32(bufflen, &cdb[6]);
-
- return scsi_execute_cmd(sdev, cdb, opf, buff, bufflen,
- ALUA_FAILOVER_TIMEOUT * HZ,
- ALUA_FAILOVER_RETRIES, &exec_args);
-}
-
/*
* submit_stpg - Issue a SET TARGET PORT GROUP command
*
@@ -518,6 +487,9 @@ static int alua_tur(struct scsi_device *sdev)
static int alua_rtpg(struct scsi_device *sdev, struct alua_port_group *pg)
{
struct scsi_sense_hdr sense_hdr;
+ const struct scsi_exec_args exec_args = {
+ .sshdr = &sense_hdr,
+ };
struct alua_port_group *tmp_pg;
int len, k, off, bufflen = ALUA_RTPG_SIZE;
int group_id_old, state_old, pref_old, valid_states_old;
@@ -549,7 +521,17 @@ static int alua_rtpg(struct scsi_device *sdev, struct alua_port_group *pg)
retry:
err = 0;
- retval = submit_rtpg(sdev, buff, bufflen, &sense_hdr, pg->flags);
+ retval = scsi_report_tpgs(sdev, &buff, &bufflen,
+ !(pg->flags & ALUA_RTPG_EXT_HDR_UNSUPP),
+ ALUA_FAILOVER_TIMEOUT * HZ,
+ ALUA_FAILOVER_RETRIES, &exec_args);
+ if (!buff) {
+ sdev_printk(KERN_WARNING, sdev,
+ "%s: kmalloc buffer failed\n", __func__);
+ /* Temporary failure, bypass */
+ pg->expiry = 0;
+ return SCSI_DH_DEV_TEMP_BUSY;
+ }
if (retval) {
/*
@@ -581,7 +563,7 @@ static int alua_rtpg(struct scsi_device *sdev, struct alua_port_group *pg)
}
/*
- * submit_rtpg() has failed on existing arrays
+ * scsi_report_tpgs() has failed on existing arrays
* when requesting extended header info, and
* the array doesn't support extended headers,
* even though it shouldn't according to T10.
@@ -629,21 +611,6 @@ static int alua_rtpg(struct scsi_device *sdev, struct alua_port_group *pg)
len = get_unaligned_be32(&buff[0]) + 4;
- if (len > bufflen) {
- /* Resubmit with the correct length */
- kfree(buff);
- bufflen = len;
- buff = kmalloc(bufflen, GFP_KERNEL);
- if (!buff) {
- sdev_printk(KERN_WARNING, sdev,
- "%s: kmalloc buffer failed\n",__func__);
- /* Temporary failure, bypass */
- pg->expiry = 0;
- return SCSI_DH_DEV_TEMP_BUSY;
- }
- goto retry;
- }
-
orig_transition_tmo = pg->transition_tmo;
if ((buff[4] & RTPG_FMT_MASK) == RTPG_FMT_EXT_HDR && buff[5] != 0)
pg->transition_tmo = buff[5];
diff --git a/drivers/scsi/scsi_lib.c b/drivers/scsi/scsi_lib.c
index f695a6605815..ebbfe3238d97 100644
--- a/drivers/scsi/scsi_lib.c
+++ b/drivers/scsi/scsi_lib.c
@@ -3581,6 +3581,58 @@ int scsi_vpd_tpg_id(struct scsi_device *sdev, int *rel_id)
}
EXPORT_SYMBOL(scsi_vpd_tpg_id);
+/**
+ * scsi_report_tpgs - Issue a REPORT TARGET PORT GROUPS command
+ * @sdev: SCSI device to send the command to
+ * @buff: buffer for the parameter data, replaced if it is too small
+ * @bufflen: length of @buff
+ * @ext_hdr: request the extended header parameter data format
+ * @timeout: command timeout in jiffies
+ * @retries: number of times to retry the command in the midlayer
+ * @args: optional arguments for scsi_execute_cmd()
+ *
+ * Resend the command with a larger buffer if the parameter data does not
+ * fit.
+ *
+ * Return: the result of the last command, or -ENOMEM with *@buff set to
+ * NULL if a larger buffer could not be allocated.
+ */
+int scsi_report_tpgs(struct scsi_device *sdev, unsigned char **buff,
+ int *bufflen, bool ext_hdr, int timeout, int retries,
+ const struct scsi_exec_args *args)
+{
+ blk_opf_t opf = REQ_OP_DRV_IN | REQ_FAILFAST_DEV |
+ REQ_FAILFAST_TRANSPORT | REQ_FAILFAST_DRIVER;
+ u8 cdb[MAX_COMMAND_SIZE] = { };
+ int ret, len;
+
+ cdb[0] = MAINTENANCE_IN;
+ cdb[1] = MI_REPORT_TARGET_PGS;
+ if (ext_hdr)
+ cdb[1] |= MI_EXT_HDR_PARAM_FMT;
+
+ retry:
+ put_unaligned_be32(*bufflen, &cdb[6]);
+ ret = scsi_execute_cmd(sdev, cdb, opf, *buff, *bufflen, timeout,
+ retries, args);
+ if (ret)
+ return ret;
+
+ len = get_unaligned_be32(&(*buff)[0]) + 4;
+ if (len > *bufflen) {
+ /* Resubmit with the correct length */
+ kfree(*buff);
+ *bufflen = len;
+ *buff = kmalloc(len, GFP_KERNEL);
+ if (!*buff)
+ return -ENOMEM;
+ goto retry;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(scsi_report_tpgs);
+
/**
* scsi_set_sense - build sense data for a command
* @scmd: scsi command for which the sense should be formatted
diff --git a/include/scsi/scsi_device.h b/include/scsi/scsi_device.h
index e6fd6bc30644..691275db0677 100644
--- a/include/scsi/scsi_device.h
+++ b/include/scsi/scsi_device.h
@@ -583,6 +583,9 @@ extern void sdev_enable_disk_events(struct scsi_device *sdev);
extern int scsi_vpd_lun_id(struct scsi_device *, char *, size_t);
extern int scsi_vpd_lun_serial(struct scsi_device *, char *, size_t);
extern int scsi_vpd_tpg_id(struct scsi_device *, int *);
+int scsi_report_tpgs(struct scsi_device *sdev, unsigned char **buff,
+ int *bufflen, bool ext_hdr, int timeout, int retries,
+ const struct scsi_exec_args *args);
#ifdef CONFIG_PM
extern int scsi_autopm_get_device(struct scsi_device *);
diff --git a/include/scsi/scsi_proto.h b/include/scsi/scsi_proto.h
index 9c7d31aa6bae..6826f2d74407 100644
--- a/include/scsi/scsi_proto.h
+++ b/include/scsi/scsi_proto.h
@@ -145,6 +145,9 @@
#define MI_MANAGEMENT_PROTOCOL_IN 0x10
/* value for MI_REPORT_TARGET_PGS ext header */
#define MI_EXT_HDR_PARAM_FMT 0x20
+/* format field in byte 4 of the REPORT TARGET PORT GROUPS parameter data */
+#define RTPG_FMT_MASK 0x70
+#define RTPG_FMT_EXT_HDR 0x10
/* values for maintenance out */
#define MO_SET_IDENTIFYING_INFORMATION 0x06
#define MO_SET_TARGET_PGS 0x0a
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH v2 2/4] scsi: core: Read the ALUA access state when a device is scanned
2026-10-28 23:08 [PATCH v2 0/4] scsi: Bind sd to LUs in the ALUA unavailable state Brian Bunker
2026-10-28 23:08 ` [PATCH v2 1/4] scsi: core: Move REPORT TARGET PORT GROUPS into the SCSI core Brian Bunker
@ 2026-10-28 23:08 ` Brian Bunker
2026-10-28 23:08 ` [PATCH v2 3/4] scsi: scsi_dh_alua: Start a port group in the scanned access state Brian Bunker
2026-10-28 23:08 ` [PATCH v2 4/4] scsi: core: Match ULDs to logical units in the ALUA unavailable state Brian Bunker
3 siblings, 0 replies; 6+ messages in thread
From: Brian Bunker @ 2026-10-28 23:08 UTC (permalink / raw)
To: linux-scsi
Cc: Martin K . Petersen, James E . J . Bottomley, Hannes Reinecke,
Bart Van Assche, Krishna Kant
sdev->access_state is only set by a device handler, and the ALUA
device handler reads it with an RTPG it queues when it attaches, which
normally completes after the device has been registered and matched to
a driver. The SCSI core therefore has no ALUA state for a device while
it decides whether to bind an upper-level driver to it.
For a device that reports TPGS support, send REPORT TARGET PORT GROUPS
from scsi_add_lun(), after the VPD pages have been read, and set
sdev->access_state from the descriptor of the target port group of
this path. UNIT ATTENTIONs, which a target typically reports first
after a login or an ALUA state change, are retried, and the command is
resent without the extended header if the target rejects it. If the
command fails, e.g. with LOGICAL UNIT NOT SUPPORTED because no LU is
mapped at the LUN, sdev->access_state is left as it is.
The command uses the same timeout as the INQUIRY of the scan. The ALUA
device handler keeps updating sdev->access_state afterwards as before.
Suggested-by: Hannes Reinecke <hare@kernel.org>
Co-developed-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Brian Bunker <brian@purestorage.com>
---
drivers/scsi/scsi_scan.c | 94 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 94 insertions(+)
diff --git a/drivers/scsi/scsi_scan.c b/drivers/scsi/scsi_scan.c
index 8aae12fd38f3..a21dee1d9e9e 100644
--- a/drivers/scsi/scsi_scan.c
+++ b/drivers/scsi/scsi_scan.c
@@ -861,6 +861,93 @@ static int scsi_probe_lun(struct scsi_device *sdev, unsigned char *inq_result,
return 0;
}
+/**
+ * scsi_read_access_state - Read the ALUA state of a device
+ * @sdev: SCSI device that supports target port groups
+ *
+ * Send REPORT TARGET PORT GROUPS and set sdev->access_state to the
+ * asymmetric access state that the device reports for the target port
+ * group of this path, as identified in VPD page 0x83. If the command
+ * fails, e.g. with LOGICAL UNIT NOT SUPPORTED because no LU is mapped at
+ * this LUN for this I_T nexus, sdev->access_state is left unchanged. The
+ * ALUA device handler, if attached, keeps the state up to date later.
+ */
+static void scsi_read_access_state(struct scsi_device *sdev)
+{
+ /*
+ * Each UNIT ATTENTION reports a different condition queued for this
+ * I_T nexus, e.g. a power on or reset after login followed by ALUA
+ * state and INQUIRY data changes, so retry until the target answers
+ * the command itself.
+ */
+ struct scsi_failure failure_defs[] = {
+ {
+ .sense_key = UNIT_ATTENTION,
+ .sense_code = SCMD_FAILURE_SENSE_CODE_ANY,
+ .allowed = 5,
+ .result = SAM_STAT_CHECK_CONDITION,
+ },
+ {}
+ };
+ struct scsi_failures failures = {
+ .failure_definitions = failure_defs,
+ };
+ struct scsi_sense_hdr sshdr;
+ const struct scsi_exec_args exec_args = {
+ .sshdr = &sshdr,
+ .failures = &failures,
+ };
+ /* scsi_report_tpgs() replaces the buffer if this is too small */
+ int bufflen = 128;
+ int group_id, len, k, off;
+ unsigned char *buff, *desc;
+ bool ext_hdr = true;
+ int ret;
+
+ group_id = scsi_vpd_tpg_id(sdev, NULL);
+ if (group_id < 0)
+ return;
+
+ buff = kzalloc(bufflen, GFP_KERNEL);
+ if (!buff)
+ return;
+
+ retry:
+ ret = scsi_report_tpgs(sdev, &buff, &bufflen, ext_hdr,
+ HZ / 2 + HZ * scsi_inq_timeout, 3, &exec_args);
+ if (ret) {
+ /*
+ * Some targets reject the extended header format with
+ * ILLEGAL REQUEST (see alua_rtpg()), so retry without it,
+ * unless the reason is that no LU is mapped at this LUN.
+ */
+ if (ret > 0 && ext_hdr && scsi_sense_valid(&sshdr) &&
+ sshdr.sense_key == ILLEGAL_REQUEST &&
+ sshdr.sense_code != LU_NOT_SUPPORTED) {
+ ext_hdr = false;
+ goto retry;
+ }
+ goto out;
+ }
+
+ len = get_unaligned_be32(&buff[0]) + 4;
+ if ((buff[4] & RTPG_FMT_MASK) == RTPG_FMT_EXT_HDR)
+ k = 8;
+ else
+ k = 4;
+
+ for (desc = buff + k; k + 8 <= len; k += off, desc += off) {
+ off = 8 + (desc[7] * 4);
+ if (get_unaligned_be16(&desc[2]) == group_id) {
+ sdev->access_state = desc[0];
+ break;
+ }
+ }
+
+ out:
+ kfree(buff);
+}
+
/**
* scsi_add_lun - allocate and fully initialize a scsi_device
* @sdev: holds information to be stored in the new scsi_device
@@ -1126,6 +1213,13 @@ static int scsi_add_lun(struct scsi_device *sdev, unsigned char *inq_result,
scsi_cdl_check(sdev);
+ /*
+ * Read the ALUA state before the device is registered, so that
+ * scsi_bus_match() can use it. This needs VPD page 0x83.
+ */
+ if (scsi_device_tpgs(sdev))
+ scsi_read_access_state(sdev);
+
sdev->max_queue_depth = sdev->queue_depth;
WARN_ON_ONCE(sdev->max_queue_depth > sdev->budget_map.depth);
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH v2 3/4] scsi: scsi_dh_alua: Start a port group in the scanned access state
2026-10-28 23:08 [PATCH v2 0/4] scsi: Bind sd to LUs in the ALUA unavailable state Brian Bunker
2026-10-28 23:08 ` [PATCH v2 1/4] scsi: core: Move REPORT TARGET PORT GROUPS into the SCSI core Brian Bunker
2026-10-28 23:08 ` [PATCH v2 2/4] scsi: core: Read the ALUA access state when a device is scanned Brian Bunker
@ 2026-10-28 23:08 ` Brian Bunker
2026-10-28 23:19 ` sashiko-bot
2026-10-28 23:08 ` [PATCH v2 4/4] scsi: core: Match ULDs to logical units in the ALUA unavailable state Brian Bunker
3 siblings, 1 reply; 6+ messages in thread
From: Brian Bunker @ 2026-10-28 23:08 UTC (permalink / raw)
To: linux-scsi
Cc: Martin K . Petersen, James E . J . Bottomley, Hannes Reinecke,
Bart Van Assche, Krishna Kant
A new port group starts out active/optimized until the first RTPG of
the handler completes, so alua_prep_fn() lets I/O through to a path
that is actually unavailable. For a device whose driver is bound
straight away, the first I/O, typically the partition table read, then
reaches the target and fails with LOGICAL UNIT NOT ACCESSIBLE, TARGET
PORT IN UNAVAILABLE STATE, logging an I/O error.
The SCSI core now reads sdev->access_state when the device is scanned,
before the handler attaches. Start a new port group in that state. If
the core did not read one, sdev->access_state is zero, which is
active/optimized, so nothing changes for those devices.
Co-developed-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Brian Bunker <brian@purestorage.com>
---
drivers/scsi/device_handler/scsi_dh_alua.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/drivers/scsi/device_handler/scsi_dh_alua.c b/drivers/scsi/device_handler/scsi_dh_alua.c
index 06eb35cc9d7c..15471e7bf19a 100644
--- a/drivers/scsi/device_handler/scsi_dh_alua.c
+++ b/drivers/scsi/device_handler/scsi_dh_alua.c
@@ -208,7 +208,14 @@ static struct alua_port_group *alua_alloc_pg(struct scsi_device *sdev,
}
pg->group_id = group_id;
pg->tpgs = tpgs;
- pg->state = SCSI_ACCESS_STATE_OPTIMAL;
+ /*
+ * Start in the access state the SCSI core read when the device was
+ * scanned. That is active/optimized if it did not read one, as it
+ * was before. alua_prep_fn() then already fails I/O to a path that
+ * is unavailable before the first RTPG of the handler completes.
+ */
+ pg->state = FIELD_GET(SCSI_ACCESS_STATE_MASK, sdev->access_state);
+ pg->pref = FIELD_GET(SCSI_ACCESS_STATE_PREFERRED, sdev->access_state);
pg->valid_states = TPGS_SUPPORT_ALL;
if (optimize_stpg)
pg->flags |= ALUA_OPTIMIZE_STPG;
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH v2 3/4] scsi: scsi_dh_alua: Start a port group in the scanned access state
2026-10-28 23:08 ` [PATCH v2 3/4] scsi: scsi_dh_alua: Start a port group in the scanned access state Brian Bunker
@ 2026-10-28 23:19 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-10-28 23:19 UTC (permalink / raw)
To: Brian Bunker, linux-scsi
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] scsi: scsi_dh_alua: initial RTPG dropped in alua_bus_attach() leaves pg->state stale
--
commit 82a8c8c9b2d2916e1a4b1a2ea0671b7c6f85346c
Author: Brian Bunker <brian@purestorage.com>
scsi: scsi_dh_alua: Start a port group in the scanned access state
This commit initializes a newly allocated ALUA port group's state and
preferred bit from the scan-time access_state stored on the scsi_device
rather than always starting in SCSI_ACCESS_STATE_OPTIMAL.
> diff --git a/drivers/scsi/device_handler/scsi_dh_alua.c b/drivers/scsi/device_handler/scsi_dh_alua.c
> index 06eb35cc9d7cb..15471e7bf19a5 100644
> --- a/drivers/scsi/device_handler/scsi_dh_alua.c
> +++ b/drivers/scsi/device_handler/scsi_dh_alua.c
> @@ -208,7 +208,14 @@ static struct alua_port_group *alua_alloc_pg(struct scsi_device *sdev,
> }
> pg->group_id = group_id;
> pg->tpgs = tpgs;
> - pg->state = SCSI_ACCESS_STATE_OPTIMAL;
> + /*
> + * Start in the access state the SCSI core read when the device was
> + * scanned. That is active/optimized if it did not read one, as it
> + * was before. alua_prep_fn() then already fails I/O to a path that
> + * is unavailable before the first RTPG of the handler completes.
> + */
> + pg->state = FIELD_GET(SCSI_ACCESS_STATE_MASK, sdev->access_state);
> + pg->pref = FIELD_GET(SCSI_ACCESS_STATE_PREFERRED, sdev->access_state);
[Severity: High]
Does the initial RTPG of the handler actually get queued when attaching via
alua_bus_attach()?
In drivers/scsi/device_handler/scsi_dh_alua.c, alua_bus_attach() calls
alua_initialize() before assigning sdev->handler_data:
alua_bus_attach()
err = alua_initialize(sdev, h);
if (err != SCSI_DH_OK && err != SCSI_DH_DEV_OFFLINED)
goto failed;
sdev->handler_data = h;
During alua_initialize() -> alua_check_vpd(), after alua_alloc_pg() sets
pg->state from sdev->access_state, alua_check_vpd() calls
alua_rtpg_queue(pg, sdev, NULL, true). However, alua_rtpg_queue() checks
sdev->handler_data before scheduling pg->rtpg_work:
if (pg->rtpg_sdev == NULL) {
struct alua_dh_data *h = sdev->handler_data;
rcu_read_lock();
if (h && rcu_dereference(h->pg) == pg) {
pg->interval = 0;
pg->flags |= ALUA_PG_RUN_RTPG;
kref_get(&pg->kref);
pg->rtpg_sdev = sdev;
start_queue = 1;
}
rcu_read_unlock();
}
Because sdev->handler_data is still NULL at this point, h is NULL and the
initial RTPG work is silently dropped.
Previously, pg->state started as SCSI_ACCESS_STATE_OPTIMAL, so I/O was
allowed through alua_prep_fn() and any sense data could trigger
alua_check_sense() after sdev->handler_data was populated.
With pg->state now starting in the scan-time sdev->access_state (and with
scan-time RTPG having already consumed pending unit attentions):
1. If sdev->access_state was UNAVAILABLE or STANDBY at scan time and the
port group transitions to OPTIMAL before alua_bus_attach() runs (or when
the handler is attached later via scsi_dh_attach()), pg->state starts as
UNAVAILABLE or STANDBY. Because the initial RTPG is dropped and
alua_prep_fn() immediately rejects non-passthrough I/O with BLK_STS_IOERR
before it reaches the target, can the path remain stuck in the stale
state?
2. If sdev->access_state was SCSI_ACCESS_STATE_TRANSITIONING at scan time,
won't dropping the initial RTPG prevent alua_rtpg_work from polling the
port group or enforcing the transition timeout?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261028230807.61578-1-brian@purestorage.com?part=3
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v2 4/4] scsi: core: Match ULDs to logical units in the ALUA unavailable state
2026-10-28 23:08 [PATCH v2 0/4] scsi: Bind sd to LUs in the ALUA unavailable state Brian Bunker
` (2 preceding siblings ...)
2026-10-28 23:08 ` [PATCH v2 3/4] scsi: scsi_dh_alua: Start a port group in the scanned access state Brian Bunker
@ 2026-10-28 23:08 ` Brian Bunker
3 siblings, 0 replies; 6+ messages in thread
From: Brian Bunker @ 2026-10-28 23:08 UTC (permalink / raw)
To: linux-scsi
Cc: Martin K . Petersen, James E . J . Bottomley, Hannes Reinecke,
Bart Van Assche, Krishna Kant
scsi_bus_match() only matches devices with peripheral qualifier 000b,
so a LU that is in the ALUA unavailable state on a target port when
the host scans it never gets an upper-level driver on that path. When
the target port becomes available again, nothing binds sd and the path
stays unusable until the device is deleted and rescanned. A host that
was already running when the path became unavailable keeps sd bound,
so the result depends on when the host scanned.
Peripheral qualifier 001b cannot be matched on its own: an unmapped
LUN reports it as well, and some targets report every possible LUN in
REPORT LUNS. Binding sd to all of those would create disks with no
backing LU.
The SCSI core now reads the access state of a device that supports
target port groups with REPORT TARGET PORT GROUPS before the device is
registered, and an unmapped LUN fails that command. SPC-4 requires a LU
in the unavailable state to report PQ 001b, and the command shows that
the LU is mapped, so record in the device whether it reported PQ 001b
and its access state could be read, and match such a device. sd then
binds to the unavailable path like it does to a standby path, the path
stays in the multipath map as a failed path, and the existing UNIT
ATTENTION handling brings it back when the target port becomes
available.
The decision is made once, at scan time, and not from access_state when
the match is made. access_state follows the ALUA state, but the
peripheral qualifier it relates to does not change after the scan. If
the match depended on access_state, a device scanned in the unavailable
state would stop matching as soon as its path became available, e.g.
when the upper-level driver module is loaded later, or the driver is
rebound, or the ALUA device handler updates access_state before the
driver is probed. It also does not depend on the state that the command
returns, which can already be available if the path recovered between
the INQUIRY and the command.
This is the same situation as a path in the standby state, to which sd
already binds: SPC-4 requires READ CAPACITY in neither state. The
unavailable state does not require MODE SENSE either, which the standby
state does.
The cached INQUIRY data in sysfs keeps the peripheral qualifier
reported at scan time, like the rest of the INQUIRY data; access_state
shows the current ALUA state.
Co-developed-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Krishna Kant <krishna.kant@purestorage.com>
Signed-off-by: Brian Bunker <brian@purestorage.com>
---
drivers/scsi/scsi_scan.c | 26 +++++++++++++++++++++-----
drivers/scsi/scsi_sysfs.c | 14 +++++++++++++-
include/scsi/scsi_device.h | 6 ++++++
3 files changed, 40 insertions(+), 6 deletions(-)
diff --git a/drivers/scsi/scsi_scan.c b/drivers/scsi/scsi_scan.c
index a21dee1d9e9e..b78b257b4b24 100644
--- a/drivers/scsi/scsi_scan.c
+++ b/drivers/scsi/scsi_scan.c
@@ -871,8 +871,10 @@ static int scsi_probe_lun(struct scsi_device *sdev, unsigned char *inq_result,
* fails, e.g. with LOGICAL UNIT NOT SUPPORTED because no LU is mapped at
* this LUN for this I_T nexus, sdev->access_state is left unchanged. The
* ALUA device handler, if attached, keeps the state up to date later.
+ *
+ * Returns true if the state was read.
*/
-static void scsi_read_access_state(struct scsi_device *sdev)
+static bool scsi_read_access_state(struct scsi_device *sdev)
{
/*
* Each UNIT ATTENTION reports a different condition queued for this
@@ -902,15 +904,16 @@ static void scsi_read_access_state(struct scsi_device *sdev)
int group_id, len, k, off;
unsigned char *buff, *desc;
bool ext_hdr = true;
+ bool found = false;
int ret;
group_id = scsi_vpd_tpg_id(sdev, NULL);
if (group_id < 0)
- return;
+ return false;
buff = kzalloc(bufflen, GFP_KERNEL);
if (!buff)
- return;
+ return false;
retry:
ret = scsi_report_tpgs(sdev, &buff, &bufflen, ext_hdr,
@@ -940,12 +943,14 @@ static void scsi_read_access_state(struct scsi_device *sdev)
off = 8 + (desc[7] * 4);
if (get_unaligned_be16(&desc[2]) == group_id) {
sdev->access_state = desc[0];
+ found = true;
break;
}
}
out:
kfree(buff);
+ return found;
}
/**
@@ -1217,8 +1222,19 @@ static int scsi_add_lun(struct scsi_device *sdev, unsigned char *inq_result,
* Read the ALUA state before the device is registered, so that
* scsi_bus_match() can use it. This needs VPD page 0x83.
*/
- if (scsi_device_tpgs(sdev))
- scsi_read_access_state(sdev);
+ if (scsi_device_tpgs(sdev)) {
+ bool read = scsi_read_access_state(sdev);
+
+ /*
+ * SPC-4 requires PQ 001b in the ALUA unavailable state, and
+ * an unmapped LUN fails the command, so take this to be a
+ * mapped LU that is unavailable. Decide once, like the
+ * peripheral qualifier itself: access_state changes when the
+ * ALUA state does.
+ */
+ sdev->match_unavailable =
+ read && sdev->inq_periph_qual == SCSI_INQ_PQ_NOT_CON;
+ }
sdev->max_queue_depth = sdev->queue_depth;
WARN_ON_ONCE(sdev->max_queue_depth > sdev->budget_map.depth);
diff --git a/drivers/scsi/scsi_sysfs.c b/drivers/scsi/scsi_sysfs.c
index fe54792ed69e..4866b5f35aed 100644
--- a/drivers/scsi/scsi_sysfs.c
+++ b/drivers/scsi/scsi_sysfs.c
@@ -539,7 +539,19 @@ static int scsi_bus_match(struct device *dev, const struct device_driver *gendrv
sdp = to_scsi_device(dev);
if (sdp->no_uld_attach)
return 0;
- return (sdp->inq_periph_qual == SCSI_INQ_PQ_CON)? 1: 0;
+ if (sdp->inq_periph_qual == SCSI_INQ_PQ_CON)
+ return 1;
+ /*
+ * Peripheral qualifier 001b is also what a LU reports while it is in
+ * the ALUA unavailable state on this target port (SPC-4 5.15.2.4.5).
+ * scsi_add_lun() reads that state with REPORT TARGET PORT GROUPS
+ * before the device is registered and records whether to match it.
+ * A LUN that is not mapped reports 001b too, but fails REPORT TARGET
+ * PORT GROUPS and so is not matched. The decision is not made from
+ * access_state here, as that changes with the ALUA state while the
+ * peripheral qualifier does not.
+ */
+ return sdp->match_unavailable;
}
static int scsi_bus_uevent(const struct device *dev, struct kobj_uevent_env *env)
diff --git a/include/scsi/scsi_device.h b/include/scsi/scsi_device.h
index 691275db0677..b9540a412ae8 100644
--- a/include/scsi/scsi_device.h
+++ b/include/scsi/scsi_device.h
@@ -196,6 +196,12 @@ struct scsi_device {
*/
unsigned is_ata:1;
+ /*
+ * Set if a ULD should be matched to a PQ 001b device in the ALUA
+ * unavailable state.
+ */
+ unsigned match_unavailable:1;
+
unsigned removable:1;
unsigned changed:1; /* Data invalid due to media change */
unsigned busy:1; /* Used to prevent races */
--
2.56.0
^ permalink raw reply related [flat|nested] 6+ messages in thread