* [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements
@ 2026-08-01 11:09 Stefan Haberland
2026-08-01 11:09 ` [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful Stefan Haberland
` (17 more replies)
0 siblings, 18 replies; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Extent Space Efficient (ESE) volumes allocate a track on its first write.
Today the first write to an unallocated track fails with No Record Found
(or similar), the driver formats the track and retries the write.
That NRF loop dominates the write path of a freshly provisioned volume.
This series lets the driver write and allocate a track in a single
operation using WRITE_FULL_TRACK, so the common "write to a
not-yet-allocated track" case avoids the NRF penalty.
Because full-track writes are only a win while a volume is still sparse, an
adaptive heuristic (exposed through a single sysfs knob) probes
the workload and falls back to normal writes once the device is mostly
allocated. On top of that the series adds an on-disk format label so an
ESE volume can be recognised without querying the hardware, and re-enables
discard for ESE volumes so freed space can be returned to the pool.
The sysfs knob is 0..100. 0 pins normal writes, 100 pins full-track,
and the default of 50 enables the heuristic; the adaptive range
interpolates the heuristic parameters between the two ends.
Patch 17 releases whole extents only: a discard range is rounded inward to
extent boundaries and partially covered boundary tracks are dropped, so an
extent shared with a live allocation is never released. Sub-extent discards
that cover no whole extent are rejected rather than over-released and
corrupting data.
Patch 18 fixes a use-after-free: the CCW build path read the base address
and LSS directly from conf.ned, which the reload worker can free
concurrently with I/O. The values are now read from the copies kept in the
device uid / private structure, refreshed under the ccwdev lock when the
configuration is (re)read.
Fixes (pre-existing):
01 Do not complete a failed ESE read as successful
02 Propagate partial completion length across ERP recovery
03 Guard sysfs discipline callbacks against unallocated private data
v5->v6:
- New patch 01 (prepended pre-existing fix): dasd_int_handler() no longer
completes a failed ESE read as DASD_CQR_SUCCESS. ese_read() can return
an error before it has zeroed the destination buffer, which would hand
the block layer stale memory; it now fails the request through the
normal error path.
- Patch 03 (sysfs guard): query_host_access() now also guards
private->lcu, which is NULL in the same set_online window while
host_access_count is world-readable; the added !private guard alone did
not cover the private->lcu->pav dereference.
- Patch 10 (build_cp builder): the newly added
dasd_eckd_build_cp_tpm_writefulltrack() is marked __maybe_unused
(removed again in patch 11, where it is first called) so the
intermediate commits do not emit -Wunused-function / break a
CONFIG_WERROR build.
- Patch 11 (WRITE_FULL_TRACK): dasd_generic_requeue_all_requests() now
skips an aborted (replaced) request instead of requeuing it - the
request is retired by its full-track replacement, so requeuing it here
would double-handle the block request. (The shared flush path already
handled this via __dasd_cleanup_cqr().)
- Patch 11 (WRITE_FULL_TRACK): the ERP retry request now inherits
filldata (dasd_3990_erp_add_erp), so an ERP retry of a full-track write
stays recognised as one and is failed rather than misrouted back into
ese_format() - which would rebuild a fresh full-track write and could
loop on a persistent INV_TRACK_FORMAT. ese_format() also fails fast on
a permanent -EINVAL build error instead of retrying it.
- Patch 13 (adaptive heuristic): the NRF-rate computation uses a u64
intermediate so nrf * 1000 cannot overflow int, and the FT0_STABLE
branches now re-assert fulltrack = 0 so a value left behind by a racing
sysfs full_track_bias write self-corrects instead of leaving the device
in full-track mode.
- Patch 15 (detect ESE from label): the rewritten dasd_eckd_is_ese() now
guards a NULL device->private, matching its sibling
dasd_eckd_on_demand_format()
- Patch 17 (re-enable discard): a discard that covers no whole extent is
now completed as a benign no-op (BLK_STS_OK) instead of
BLK_STS_NOTSUPP.
Discard is advisory, and -EOPNOTSUPP only ever originates from the
discard builder's "no whole extent to release" paths; returning NOTSUPP
made filesystems treat the device as lacking discard and stop issuing
it. The request is ended after the queue lock is dropped.
dasd_eckd_disc_limits() also guards a zero extent size before the
modulo, so malformed RDC data cannot divide by zero at bring-up.
v4->v5:
- The sysfs private-data guard (patch 02) was moved out of the callers
(the DASD_DEFINE_ATTR() macro and the individual sysfs handlers) into
each discipline callback that actually dereferences device->private, so
a caller need not know whether a callee uses private. This also covers
the callbacks that were still unguarded: is_ese(), the extent-pool /
space attributes (ext_size(), ext_pool_id(), space_configured(),
space_allocated(), logical_capacity(), ext_pool_warn_thrshld(),
ext_pool_cap_at_warnlevel(), ext_pool_oos()), hpf_enabled(),
reset_path() and query_host_access().
- Full-track channel-program builder (patch 09): crosses_page() and
reserve_nocross() now use offset_in_page(); reserve_nocross() fails
atomically without advancing the fill pointer; datasize simplified to
trkcount * tlf; plus minor cleanups (declaration order, dead code,
comments).
- full_track_bias_store() (patch 12) received the same guard-in-callee
treatment: the offline check was removed from the store, and
dasd_ese_adaptive() now verifies device->discipline before invoking the
is_ese callback. The heuristic interpolation endpoints d50/d100 were
renamed v50/v100 for clarity.
- On-disk format label (patch 13): the informational per-format string
now records the running kernel version (uname -r) instead of a fixed
driver name; the field was renamed kernel_version and enlarged to
64 bytes.
- Discard (patch 16): build_cp_discard() now uses roundup()/rounddown()
with a named inclusive last track for the extent-alignment (no
functional change); the count_exts() comment was trimmed to just
describe the formula.
v3->v4:
- Two pre-existing bugs surfaced during review are prepended as fixes:
- ERP recovery now carries proc_bytes back to the original request,
so a partially completed ESE read that is recovered through the ERP
chain no longer completes the whole request and returns zeroed data
for the unread remainder (patch 01).
- the ese / on_demand_formatting sysfs attributes now check
device->private before dereferencing it, closing an unprivileged
NULL-pointer read during the set_online window (patch 02).
- The dasd_alloc_device() GFP_ATOMIC->GFP_KERNEL conversion was split out
of the ESE infrastructure patch into its own patch (04): it runs in
process context and is an independent change.
- New patch (05) names the Extended Address Volume track-address shifts
used by set_ch_t()/set_chr_t() instead of open-coding the constants.
- Full-track write path: the ESE format handler's error, partial
completion and abort exits now cancel the device timer and schedule the
bottom halves (a failed request previously waited for the timeout); the
exits were consolidated behind shared out:/out_retry: labels. free_cp()
now releases the bounce buffers of an aborted (format-replaced) write
instead of leaking them. build_cp_tpm_writefulltrack() budgets
page-boundary padding in the fill buffer, and its in-loop track-end
length matches the physical track.
- Adaptive mode selection: full_track_bias no longer enables full-track
writes on non-ESE volumes by default (only an explicit ft_bias=100
forces it); the sysfs store rejects an offline device, and the
heuristic re-asserts the endpoint mode per I/O so a racing sysfs write
cannot wedge it.
- On-disk format label: a failed full format invalidates the cached label
so is_ese() falls back to the hardware field, and the fulltrack
heuristic is (re)applied after the format commits rather than before.
- Discard: discard is no longer advertised for raw-track-access (USERAW)
volumes, which would otherwise route a discard into the raw CCW
builder.
- conf.ned fix: the cached unit address is kept in a dedicated field, so
the lockless CCW-build readers cannot observe the transient zero while
create_uid() repopulates the uid.
Stefan Haberland (18):
s390/dasd: Do not complete a failed ESE read as successful
s390/dasd: Propagate partial completion length across ERP recovery
s390/dasd: Guard sysfs discipline callbacks against unallocated
private data
s390/dasd: Optimize max blocks per request for track alignment
s390/dasd: Use GFP_KERNEL in dasd_alloc_device()
s390/dasd: Add defines for the Extended Address Volume track address
s390/dasd: Add infrastructure for ESE full-track write
s390/dasd: Add range-based format-track collision detection
s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK
s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack()
s390/dasd: Use WRITE_FULL_TRACK in ESE format handler
s390/dasd: Add full_track_bias to control fulltrack write mode
s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias
s390/dasd: Stamp a format label into newly formatted volumes
s390/dasd: Detect ESE volumes from the on-disk format label
s390/dasd: Report ESE capability and format mode at device online
s390/dasd: Re-enable discard support for ESE volumes
s390/dasd: Read cached unit address and LSS in the CCW build path
drivers/s390/block/dasd.c | 226 +++--
drivers/s390/block/dasd_3990_erp.c | 1 +
drivers/s390/block/dasd_devmap.c | 87 +-
drivers/s390/block/dasd_eckd.c | 1258 +++++++++++++++++++++++++---
drivers/s390/block/dasd_eckd.h | 68 +-
drivers/s390/block/dasd_erp.c | 11 +-
drivers/s390/block/dasd_int.h | 149 +++-
7 files changed, 1604 insertions(+), 196 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 37+ messages in thread
* [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:27 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 02/18] s390/dasd: Propagate partial completion length across ERP recovery Stefan Haberland
` (16 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
dasd_int_handler() completes an NRF read of an unallocated ESE track by
calling ese_read() and unconditionally marking the request
DASD_CQR_SUCCESS. dasd_eckd_ese_read() can return an error before it has
zeroed the destination buffer: a failed sense-data parse or a current
track outside the requested range both return early, leaving the
destination pages untouched. The request is still completed successfully,
so the block layer is handed stale / uninitialized memory instead of
zeros.
Check the ese_read() return value and fail the request through the normal
error path instead of forcing DASD_CQR_SUCCESS.
Fixes: 5e6bdd37c552 ("s390/dasd: fix data corruption for thin provisioned devices")
Cc: stable@vger.kernel.org
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
index dc1c6d5ab3be..e9f0722af104 100644
--- a/drivers/s390/block/dasd.c
+++ b/drivers/s390/block/dasd.c
@@ -1698,8 +1698,10 @@ void dasd_int_handler(struct ccw_device *cdev, unsigned long intparm,
return;
}
if (rq_data_dir(req) == READ) {
- device->discipline->ese_read(cqr, irb);
- cqr->status = DASD_CQR_SUCCESS;
+ if (device->discipline->ese_read(cqr, irb))
+ cqr->status = DASD_CQR_ERROR;
+ else
+ cqr->status = DASD_CQR_SUCCESS;
cqr->stopclk = now;
dasd_device_clear_timer(device);
dasd_schedule_device_bh(device);
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 02/18] s390/dasd: Propagate partial completion length across ERP recovery
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
2026-08-01 11:09 ` [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:21 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data Stefan Haberland
` (15 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
dasd_default_erp_postaction() copies the timing and device state from
the finished ERP request back to the original request but drops
proc_bytes. A request that was partially completed, an ESE read of a
not-yet-allocated track returns fewer bytes than requested, and then
recovered through the ERP chain loses its partial-completion length.
__dasd_cleanup_cqr() then sees proc_bytes == 0 and completes the whole
request instead of requeueing the remainder, silently returning zeroed
data for the part that was never read.
Carry proc_bytes over to the original request like the other
per-request state.
Fixes: 5e6bdd37c552 ("s390/dasd: fix data corruption for thin provisioned devices")
Cc: stable@vger.kernel.org
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_erp.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/drivers/s390/block/dasd_erp.c b/drivers/s390/block/dasd_erp.c
index 89d7516b9ec8..468f0b2cc342 100644
--- a/drivers/s390/block/dasd_erp.c
+++ b/drivers/s390/block/dasd_erp.c
@@ -123,6 +123,7 @@ struct dasd_ccw_req *dasd_default_erp_postaction(struct dasd_ccw_req *cqr)
int success;
unsigned long startclk, stopclk;
struct dasd_device *startdev;
+ unsigned int proc_bytes;
BUG_ON(cqr->refers == NULL || cqr->function == NULL);
@@ -130,6 +131,7 @@ struct dasd_ccw_req *dasd_default_erp_postaction(struct dasd_ccw_req *cqr)
startclk = cqr->startclk;
stopclk = cqr->stopclk;
startdev = cqr->startdev;
+ proc_bytes = cqr->proc_bytes;
/* free all ERPs - but NOT the original cqr */
while (cqr->refers != NULL) {
@@ -147,6 +149,7 @@ struct dasd_ccw_req *dasd_default_erp_postaction(struct dasd_ccw_req *cqr)
cqr->startclk = startclk;
cqr->stopclk = stopclk;
cqr->startdev = startdev;
+ cqr->proc_bytes = proc_bytes;
if (success)
cqr->status = DASD_CQR_DONE;
else {
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
2026-08-01 11:09 ` [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful Stefan Haberland
2026-08-01 11:09 ` [PATCH v6 02/18] s390/dasd: Propagate partial completion length across ERP recovery Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:40 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 04/18] s390/dasd: Optimize max blocks per request for track alignment Stefan Haberland
` (14 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Several sysfs show/store handlers call a discipline callback that
dereferences device->private, either directly or through the
DASD_DEFINE_ATTR() macro. During dasd_generic_set_online() the discipline
is assigned before check_device() allocates device->private, so an
unprivileged read of one of these world-readable attributes in that window
dereferences a NULL pointer and panics.
Guard the dereference inside each callback that actually touches
device->private.
Fixes: c729696bcf8b ("s390/dasd: Recognise data for ESE volumes")
Cc: stable@vger.kernel.org
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 40 ++++++++++++++++++++++++++++++++--
1 file changed, 38 insertions(+), 2 deletions(-)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 073795ea7cd0..7ce8a82d951d 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -1491,6 +1491,8 @@ static void dasd_eckd_reset_path(struct dasd_device *device, __u8 pm)
struct dasd_eckd_private *private = device->private;
unsigned long flags;
+ if (!private)
+ return;
if (!private->fcx_max_data)
private->fcx_max_data = get_fcx_max_data(device);
spin_lock_irqsave(get_ccwdev_lock(device->cdev), flags);
@@ -1646,6 +1648,9 @@ static int dasd_eckd_is_ese(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->vsq.vol_info.ese;
}
@@ -1653,6 +1658,9 @@ static int dasd_eckd_ext_pool_id(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->vsq.extent_pool_id;
}
@@ -1666,6 +1674,9 @@ static int dasd_eckd_space_configured(struct dasd_device *device)
struct dasd_eckd_private *private = device->private;
int rc;
+ if (!private)
+ return 0;
+
rc = dasd_eckd_read_vol_info(device);
return rc ? : private->vsq.space_configured;
@@ -1680,6 +1691,9 @@ static int dasd_eckd_space_allocated(struct dasd_device *device)
struct dasd_eckd_private *private = device->private;
int rc;
+ if (!private)
+ return 0;
+
rc = dasd_eckd_read_vol_info(device);
return rc ? : private->vsq.space_allocated;
@@ -1689,6 +1703,9 @@ static int dasd_eckd_logical_capacity(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->vsq.logical_capacity;
}
@@ -1831,7 +1848,11 @@ static int dasd_eckd_read_ext_pool_info(struct dasd_device *device)
static int dasd_eckd_ext_size(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
- struct dasd_ext_pool_sum eps = private->eps;
+ struct dasd_ext_pool_sum eps;
+
+ if (!private)
+ return 0;
+ eps = private->eps;
if (!eps.flags.extent_size_valid)
return 0;
@@ -1847,6 +1868,9 @@ static int dasd_eckd_ext_pool_warn_thrshld(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->eps.warn_thrshld;
}
@@ -1854,6 +1878,9 @@ static int dasd_eckd_ext_pool_cap_at_warnlevel(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->eps.flags.capacity_at_warnlevel;
}
@@ -1864,6 +1891,9 @@ static int dasd_eckd_ext_pool_oos(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->eps.flags.pool_oos;
}
@@ -5935,8 +5965,11 @@ static int dasd_eckd_query_host_access(struct dasd_device *device,
struct ccw1 *ccw;
int rc;
+ if (!private)
+ return -ENODEV;
+
/* not available for HYPER PAV alias devices */
- if (!device->block && private->lcu->pav == HYPER_PAV)
+ if (!device->block && private->lcu && private->lcu->pav == HYPER_PAV)
return -EOPNOTSUPP;
/* may not be supported by the storage server */
@@ -6801,6 +6834,9 @@ static int dasd_eckd_hpf_enabled(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
+ if (!private)
+ return 0;
+
return private->fcx_max_data ? 1 : 0;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 04/18] s390/dasd: Optimize max blocks per request for track alignment
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (2 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:20 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 05/18] s390/dasd: Use GFP_KERNEL in dasd_alloc_device() Stefan Haberland
` (13 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
With 4096-byte blocks a full ECKD track holds exactly 12 records. Lower
DASD_ECKD_MAX_BLOCKS from 190 to 180 so requests align to track
boundaries (15 full tracks); full-track I/O is more efficient than
partial-track writes, and 190 had no alignment significance and could
let a request cross a track boundary.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/s390/block/dasd_eckd.h b/drivers/s390/block/dasd_eckd.h
index f9299bd184ba..763733bcc4d2 100644
--- a/drivers/s390/block/dasd_eckd.h
+++ b/drivers/s390/block/dasd_eckd.h
@@ -131,7 +131,7 @@
/*
* Maximum number of blocks to be chained
*/
-#define DASD_ECKD_MAX_BLOCKS 190
+#define DASD_ECKD_MAX_BLOCKS 180
#define DASD_ECKD_MAX_BLOCKS_RAW 256
/*****************************************************************************
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 05/18] s390/dasd: Use GFP_KERNEL in dasd_alloc_device()
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (3 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 04/18] s390/dasd: Optimize max blocks per request for track alignment Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:21 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 06/18] s390/dasd: Add defines for the Extended Address Volume track address Stefan Haberland
` (12 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
dasd_alloc_device() runs in process context (device set_online), so its
pool allocations do not need GFP_ATOMIC. Use GFP_KERNEL instead, which is
more reliable, especially for the larger DMA allocations that later ESE
full-track work adds here.
No functional change intended.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
index e9f0722af104..9c24ae54b2d5 100644
--- a/drivers/s390/block/dasd.c
+++ b/drivers/s390/block/dasd.c
@@ -87,25 +87,25 @@ struct dasd_device *dasd_alloc_device(void)
{
struct dasd_device *device;
- device = kzalloc_obj(struct dasd_device, GFP_ATOMIC);
+ device = kzalloc_obj(struct dasd_device, GFP_KERNEL);
if (!device)
return ERR_PTR(-ENOMEM);
/* Get two pages for normal block device operations. */
- device->ccw_mem = (void *) __get_free_pages(GFP_ATOMIC | GFP_DMA, 1);
+ device->ccw_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 1);
if (!device->ccw_mem) {
kfree(device);
return ERR_PTR(-ENOMEM);
}
/* Get one page for error recovery. */
- device->erp_mem = (void *) get_zeroed_page(GFP_ATOMIC | GFP_DMA);
+ device->erp_mem = (void *)get_zeroed_page(GFP_KERNEL | GFP_DMA);
if (!device->erp_mem) {
free_pages((unsigned long) device->ccw_mem, 1);
kfree(device);
return ERR_PTR(-ENOMEM);
}
/* Get two pages for ese format. */
- device->ese_mem = (void *)__get_free_pages(GFP_ATOMIC | GFP_DMA, 1);
+ device->ese_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 1);
if (!device->ese_mem) {
free_page((unsigned long) device->erp_mem);
free_pages((unsigned long) device->ccw_mem, 1);
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 06/18] s390/dasd: Add defines for the Extended Address Volume track address
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (4 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 05/18] s390/dasd: Use GFP_KERNEL in dasd_alloc_device() Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:13 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write Stefan Haberland
` (11 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
The track address of an Extended Address Volume (more than 65520
cylinders) carries the high cylinder bits that do not fit the 16-bit cyl
field in the upper part of the head field. set_ch_t() open-codes the
corresponding shifts; name them so the encoding is explicit and can be
reused.
No functional change.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 4 ++--
drivers/s390/block/dasd_eckd.h | 8 ++++++++
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 7ce8a82d951d..8192f939aa43 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -199,8 +199,8 @@ recs_per_track(struct dasd_eckd_characteristics * rdc,
static void set_ch_t(struct ch_t *geo, __u32 cyl, __u8 head)
{
geo->cyl = (__u16) cyl;
- geo->head = cyl >> 16;
- geo->head <<= 4;
+ geo->head = cyl >> DASD_EAV_CYL_HI_SHIFT;
+ geo->head <<= DASD_EAV_HEAD_HI_SHIFT;
geo->head |= head;
}
diff --git a/drivers/s390/block/dasd_eckd.h b/drivers/s390/block/dasd_eckd.h
index 763733bcc4d2..bad7ba666370 100644
--- a/drivers/s390/block/dasd_eckd.h
+++ b/drivers/s390/block/dasd_eckd.h
@@ -146,6 +146,14 @@ struct eckd_count {
__u16 dl;
} __attribute__ ((packed));
+/*
+ * Extended Address Volume track address: the head field carries the actual
+ * head in its low-order 4 bits; the cylinder bits that do not fit the 16-bit
+ * cyl field are shifted in just above them.
+ */
+#define DASD_EAV_CYL_HI_SHIFT 16 /* cylinder bits beyond the 16-bit cyl field */
+#define DASD_EAV_HEAD_HI_SHIFT 4 /* head occupies the low-order 4 bits of head */
+
struct ch_t {
__u16 cyl;
__u16 head;
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (5 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 06/18] s390/dasd: Add defines for the Extended Address Volume track address Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:32 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection Stefan Haberland
` (10 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Add the driver internals to build WRITE_FULL_TRACK FCX channel programs
in response to unformatted tracks on ESE devices.
struct dasd_ccw_req: filldata, a pointer to the per-track metadata (an R0
record and the count records) that the WRITE_FULL_TRACK TIDAWs point at,
and format/start_trk/end_trk/collision that link a request to its
format-track guard entry so an overlapping format request can be detected.
struct dasd_device: fill_mem/fill_chunks pool for those buffers and a
zeroed nulldata page used as the data source for pad records.
struct dasd_block: ese_staging/ese_lock, a hardirq-safe staging list. An
ESE format CQR is created in the interrupt handler but has to be enqueued
on ccw_queue under queue_lock; taking queue_lock while the ccwdev_lock is
held there would invert the lock order, so the CQR is staged under ese_lock
and dasd_block_tasklet splices it onto ccw_queue. Existing locking is
unchanged.
Add CQR states DASD_CQR_ABORT/ABORTED to retire the origin CQR of a
replaced write without completing it to the block layer, and struct
eckd_r0 for the track header record.
The CCW and ESE format pools are enlarged (a full-track ITCW is roughly
twice a plain track-mode one) to keep two maximum-size requests in flight.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd.c | 93 ++++++++++++++++++++++++++++------
drivers/s390/block/dasd_eckd.c | 11 ++++
drivers/s390/block/dasd_eckd.h | 5 ++
drivers/s390/block/dasd_int.h | 19 +++++++
4 files changed, 113 insertions(+), 15 deletions(-)
diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
index 9c24ae54b2d5..50219c7ff3dc 100644
--- a/drivers/s390/block/dasd.c
+++ b/drivers/s390/block/dasd.c
@@ -91,31 +91,53 @@ struct dasd_device *dasd_alloc_device(void)
if (!device)
return ERR_PTR(-ENOMEM);
- /* Get two pages for normal block device operations. */
- device->ccw_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 1);
+ /*
+ * Four pages: a full-track ITCW is roughly twice the size of a plain
+ * track-mode one, so this keeps two maximum-size requests in flight.
+ */
+ device->ccw_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 2);
if (!device->ccw_mem) {
kfree(device);
return ERR_PTR(-ENOMEM);
}
+ /* per-request track-filler buffers (R0 + count records) */
+ device->fill_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 1);
+ if (!device->fill_mem) {
+ free_pages((unsigned long)device->ccw_mem, 2);
+ kfree(device);
+ return ERR_PTR(-ENOMEM);
+ }
/* Get one page for error recovery. */
device->erp_mem = (void *)get_zeroed_page(GFP_KERNEL | GFP_DMA);
if (!device->erp_mem) {
- free_pages((unsigned long) device->ccw_mem, 1);
+ free_pages((unsigned long)device->fill_mem, 1);
+ free_pages((unsigned long)device->ccw_mem, 2);
kfree(device);
return ERR_PTR(-ENOMEM);
}
- /* Get two pages for ese format. */
- device->ese_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 1);
+ /* sized like ccw_chunks: two max-size NRF format requests in flight */
+ device->ese_mem = (void *)__get_free_pages(GFP_KERNEL | GFP_DMA, 2);
if (!device->ese_mem) {
- free_page((unsigned long) device->erp_mem);
- free_pages((unsigned long) device->ccw_mem, 1);
+ free_page((unsigned long)device->erp_mem);
+ free_pages((unsigned long)device->fill_mem, 1);
+ free_pages((unsigned long)device->ccw_mem, 2);
+ kfree(device);
+ return ERR_PTR(-ENOMEM);
+ }
+ device->nulldata = (void *)get_zeroed_page(GFP_KERNEL | GFP_DMA);
+ if (!device->nulldata) {
+ free_page((unsigned long)device->erp_mem);
+ free_pages((unsigned long)device->fill_mem, 1);
+ free_pages((unsigned long)device->ccw_mem, 2);
+ free_pages((unsigned long)device->ese_mem, 2);
kfree(device);
return ERR_PTR(-ENOMEM);
}
- dasd_init_chunklist(&device->ccw_chunks, device->ccw_mem, PAGE_SIZE*2);
+ dasd_init_chunklist(&device->ccw_chunks, device->ccw_mem, PAGE_SIZE * 4);
+ dasd_init_chunklist(&device->fill_chunks, device->fill_mem, PAGE_SIZE * 2);
dasd_init_chunklist(&device->erp_chunks, device->erp_mem, PAGE_SIZE);
- dasd_init_chunklist(&device->ese_chunks, device->ese_mem, PAGE_SIZE * 2);
+ dasd_init_chunklist(&device->ese_chunks, device->ese_mem, PAGE_SIZE * 4);
spin_lock_init(&device->mem_lock);
atomic_set(&device->tasklet_scheduled, 0);
tasklet_init(&device->tasklet, dasd_device_tasklet,
@@ -138,9 +160,11 @@ struct dasd_device *dasd_alloc_device(void)
void dasd_free_device(struct dasd_device *device)
{
kfree(device->private);
- free_pages((unsigned long) device->ese_mem, 1);
- free_page((unsigned long) device->erp_mem);
- free_pages((unsigned long) device->ccw_mem, 1);
+ free_pages((unsigned long)device->ese_mem, 2);
+ free_page((unsigned long)device->erp_mem);
+ free_pages((unsigned long)device->fill_mem, 1);
+ free_pages((unsigned long)device->ccw_mem, 2);
+ free_page((unsigned long)device->nulldata);
kfree(device);
}
@@ -164,6 +188,8 @@ struct dasd_block *dasd_alloc_block(void)
spin_lock_init(&block->queue_lock);
INIT_LIST_HEAD(&block->format_list);
spin_lock_init(&block->format_lock);
+ INIT_LIST_HEAD(&block->ese_staging);
+ spin_lock_init(&block->ese_lock);
timer_setup(&block->timer, dasd_block_timeout, 0);
spin_lock_init(&block->profile.lock);
@@ -364,7 +390,8 @@ int _wait_for_empty_queues(struct dasd_device *device)
{
if (device->block)
return list_empty(&device->ccw_queue) &&
- list_empty(&device->block->ccw_queue);
+ list_empty(&device->block->ccw_queue) &&
+ list_empty(&device->block->ese_staging);
else
return list_empty(&device->ccw_queue);
}
@@ -1224,7 +1251,18 @@ void dasd_sfree_request(struct dasd_ccw_req *cqr, struct dasd_device *device)
unsigned long flags;
spin_lock_irqsave(&device->mem_lock, flags);
- dasd_free_chunk(&device->ccw_chunks, cqr->mem_chunk);
+ /*
+ * Free the request block from the pool it came from: smalloc() sets
+ * mem_chunk (ccw_chunks), fmalloc() leaves it NULL (ese_chunks). A
+ * full-track request also frees its track-filler buffer.
+ */
+ if (cqr->filldata)
+ dasd_free_chunk(&device->fill_chunks, cqr->filldata);
+ if (cqr->mem_chunk)
+ dasd_free_chunk(&device->ccw_chunks, cqr->mem_chunk);
+ else
+ dasd_free_chunk(&device->ese_chunks, cqr);
+
spin_unlock_irqrestore(&device->mem_lock, flags);
dasd_put_device(device);
}
@@ -1235,6 +1273,8 @@ void dasd_ffree_request(struct dasd_ccw_req *cqr, struct dasd_device *device)
unsigned long flags;
spin_lock_irqsave(&device->mem_lock, flags);
+ if (cqr->filldata)
+ dasd_free_chunk(&device->fill_chunks, cqr->filldata);
dasd_free_chunk(&device->ese_chunks, cqr);
spin_unlock_irqrestore(&device->mem_lock, flags);
dasd_put_device(device);
@@ -1885,6 +1925,10 @@ static void __dasd_process_cqr(struct dasd_device *device,
case DASD_CQR_CLEARED:
cqr->status = DASD_CQR_TERMINATED;
break;
+ case DASD_CQR_ABORT:
+ cqr->status = DASD_CQR_ABORTED;
+ cqr->callback = NULL;
+ break;
default:
dev_err(&device->cdev->dev,
"Unexpected CQR status %02x", cqr->status);
@@ -2212,6 +2256,7 @@ EXPORT_SYMBOL(dasd_add_request_tail);
void dasd_wakeup_cb(struct dasd_ccw_req *cqr, void *data)
{
spin_lock_irq(get_ccwdev_lock(cqr->startdev->cdev));
+ cqr->endclk = get_tod_clock();
cqr->callback_data = DASD_SLEEPON_END_TAG;
spin_unlock_irq(get_ccwdev_lock(cqr->startdev->cdev));
wake_up(&generic_waitq);
@@ -2773,7 +2818,8 @@ static void __dasd_process_block_ccw_queue(struct dasd_block *block,
if (cqr->status != DASD_CQR_DONE &&
cqr->status != DASD_CQR_FAILED &&
cqr->status != DASD_CQR_NEED_ERP &&
- cqr->status != DASD_CQR_TERMINATED)
+ cqr->status != DASD_CQR_TERMINATED &&
+ cqr->status != DASD_CQR_ABORTED)
continue;
if (cqr->status == DASD_CQR_TERMINATED) {
@@ -2884,6 +2930,14 @@ static void dasd_block_tasklet(unsigned long data)
atomic_set(&block->tasklet_scheduled, 0);
INIT_LIST_HEAD(&final_queue);
spin_lock_irq(&block->queue_lock);
+ /*
+ * Splice the hardirq-staged ESE format CQRs onto ccw_queue. Splice to
+ * the tail so an aborted origin request (already on ccw_queue) is
+ * retired before its format-CQR replacement completes and requeues it.
+ */
+ spin_lock(&block->ese_lock);
+ list_splice_tail_init(&block->ese_staging, &block->ccw_queue);
+ spin_unlock(&block->ese_lock);
/* Finish off requests on ccw queue */
__dasd_process_block_ccw_queue(block, &final_queue);
spin_unlock_irq(&block->queue_lock);
@@ -2943,6 +2997,15 @@ static int _dasd_requests_to_flushqueue(struct dasd_block *block,
int rc, i;
spin_lock_irqsave(&block->queue_lock, flags);
+ /*
+ * Splice any hardirq-staged ESE format CQRs onto ccw_queue first so
+ * they are seen and canceled by the walk below instead of being
+ * orphaned across this flush / state transition. Mirrors the splice
+ * in dasd_block_tasklet().
+ */
+ spin_lock(&block->ese_lock);
+ list_splice_tail_init(&block->ese_staging, &block->ccw_queue);
+ spin_unlock(&block->ese_lock);
rc = 0;
restart:
list_for_each_entry_safe(cqr, n, &block->ccw_queue, blocklist) {
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 8192f939aa43..db07fafefe04 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -204,6 +204,17 @@ static void set_ch_t(struct ch_t *geo, __u32 cyl, __u8 head)
geo->head |= head;
}
+static void set_chr_t(void *addr, __u32 cyl, __u8 head, __u8 record)
+{
+ struct chr_t *geo = addr;
+
+ geo->cyl = (__u16)cyl;
+ geo->head = cyl >> DASD_EAV_CYL_HI_SHIFT;
+ geo->head <<= DASD_EAV_HEAD_HI_SHIFT;
+ geo->head |= head;
+ geo->record = record;
+}
+
/*
* calculate failing track from sense data depending if
* it is an EAV device or not
diff --git a/drivers/s390/block/dasd_eckd.h b/drivers/s390/block/dasd_eckd.h
index bad7ba666370..0fdb92fdddc8 100644
--- a/drivers/s390/block/dasd_eckd.h
+++ b/drivers/s390/block/dasd_eckd.h
@@ -146,6 +146,11 @@ struct eckd_count {
__u16 dl;
} __attribute__ ((packed));
+struct eckd_r0 {
+ struct eckd_count count;
+ __u8 data[8];
+} __packed;
+
/*
* Extended Address Volume track address: the head field carries the actual
* head in its low-order 4 bits; the cylinder bits that do not fit the 16-bit
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index 99c253421653..0ca039126ae9 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -159,6 +159,11 @@ struct dasd_ccw_req {
void *callback_data;
unsigned int proc_bytes; /* bytes for partial completion */
unsigned int trkcount; /* count formatted tracks */
+ void *filldata; /* address of filler data */
+ struct dasd_format_entry *format;
+ sector_t start_trk;
+ sector_t end_trk;
+ bool collision;
};
/*
@@ -170,6 +175,7 @@ struct dasd_ccw_req {
#define DASD_CQR_IN_ERP 0x03 /* request is in recovery */
#define DASD_CQR_FAILED 0x04 /* request is finally failed */
#define DASD_CQR_TERMINATED 0x05 /* request was stopped by driver */
+#define DASD_CQR_ABORTED 0x06 /* request was replaced and will be deleted */
#define DASD_CQR_QUEUED 0x80 /* request is queued to be processed */
#define DASD_CQR_IN_IO 0x81 /* request is currently in IO */
@@ -177,6 +183,7 @@ struct dasd_ccw_req {
#define DASD_CQR_CLEAR_PENDING 0x83 /* request is clear pending */
#define DASD_CQR_CLEARED 0x84 /* request was cleared */
#define DASD_CQR_SUCCESS 0x85 /* request was successful */
+#define DASD_CQR_ABORT 0x86 /* request was replaced and will not be handled */
/* default expiration time*/
#define DASD_EXPIRES 300
@@ -573,9 +580,12 @@ struct dasd_device {
struct list_head ccw_queue;
spinlock_t mem_lock;
void *ccw_mem;
+ void *fill_mem;
void *erp_mem;
void *ese_mem;
+ void *nulldata;
struct list_head ccw_chunks;
+ struct list_head fill_chunks;
struct list_head erp_chunks;
struct list_head ese_chunks;
@@ -640,6 +650,15 @@ struct dasd_block {
struct list_head format_list;
spinlock_t format_lock;
atomic_t trkcount;
+
+ /*
+ * ESE format CQRs staged from hardirq, spliced into
+ * ccw_queue in dasd_block_tasklet under queue_lock. Direct enqueue from
+ * the IRQ handler would invert the queue_lock / ccwdev_lock order.
+ */
+ struct list_head ese_staging;
+ /* lock for ese_staging */
+ spinlock_t ese_lock;
};
struct dasd_attention_data {
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (6 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:36 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK Stefan Haberland
` (9 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Replace the single per-device format_entry slot with an array of 16
slots so multiple format requests can be in flight at once, and extend
struct dasd_format_entry with a start_trk/end_trk/cqr range (replacing
the single track field).
Rewrite test_and_set_format_track() to scan the array for range overlaps
instead of a trkcount snapshot, honour the early-collision flag, and
return the allocated slot to the caller.
Add dasd_req_conflict() and extend dasd_return_cqr_cb() to mark in-flight
data CQRs that overlap a just-completed format range, so the next
test_and_set_format_track() detects the conflict early.
Remove the now-obsolete trkcount snapshot in dasd_start_IO().
The detection added here only becomes active together with the
WRITE_FULL_TRACK ESE format handler later in the series: that patch routes
the format request through dasd_return_cqr_cb() (so completion runs the
overlap hook with cqr->format set) and records each request's
start_trk/end_trk range. Until then the array and the conflict check are in
place but dormant.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd.c | 29 ++++++++++++----
drivers/s390/block/dasd_eckd.c | 60 ++++++++++++++++++++++------------
drivers/s390/block/dasd_int.h | 19 +++++++++--
3 files changed, 79 insertions(+), 29 deletions(-)
diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
index 50219c7ff3dc..9cb97df0866c 100644
--- a/drivers/s390/block/dasd.c
+++ b/drivers/s390/block/dasd.c
@@ -1402,13 +1402,6 @@ int dasd_start_IO(struct dasd_ccw_req *cqr)
if (!cqr->lpm)
cqr->lpm = dasd_path_get_opm(device);
}
- /*
- * remember the amount of formatted tracks to prevent double format on
- * ESE devices
- */
- if (cqr->block)
- cqr->trkcount = atomic_read(&cqr->block->trkcount);
-
if (cqr->cpmode == 1) {
rc = ccw_device_tm_start(device->cdev, cqr->cpaddr,
(long) cqr, cqr->lpm);
@@ -2867,6 +2860,28 @@ static void __dasd_process_block_ccw_queue(struct dasd_block *block,
static void dasd_return_cqr_cb(struct dasd_ccw_req *cqr, void *data)
{
+ struct dasd_ccw_req *temp_cqr;
+ struct dasd_block *block;
+
+ /* only format CQRs are candidates */
+ if (!cqr->block || unlikely(!cqr->format))
+ goto out;
+
+ block = cqr->block;
+ /*
+ * Mark in-flight (IN_IO) CQRs that overlap this just-completed format
+ * range so they re-check in test_and_set_format on completion; FILLED
+ * or QUEUED CQRs re-check the format_list on their next round anyway.
+ */
+ list_for_each_entry(temp_cqr, &block->ccw_queue, blocklist) {
+ if (temp_cqr != cqr &&
+ temp_cqr->status != DASD_CQR_FILLED &&
+ temp_cqr->status != DASD_CQR_QUEUED &&
+ dasd_req_conflict(cqr, temp_cqr)) {
+ temp_cqr->collision = true;
+ }
+ }
+out:
dasd_schedule_block_bh(cqr->block);
}
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index db07fafefe04..577ce55da68f 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -3150,32 +3150,43 @@ static int dasd_eckd_format_device(struct dasd_device *base,
0, NULL);
}
-static bool test_and_set_format_track(struct dasd_format_entry *to_format,
- struct dasd_ccw_req *cqr)
+static bool test_and_set_format_track(sector_t start, sector_t end,
+ struct dasd_ccw_req *cqr,
+ struct dasd_block *block,
+ struct dasd_device *device,
+ struct dasd_format_entry **entry)
{
- struct dasd_block *block = cqr->block;
- struct dasd_format_entry *format;
+ struct dasd_format_entry *to_format, *format;
unsigned long flags;
bool rc = false;
+ int i = 0;
+ /* marked as a collision by dasd_return_cqr_cb last round: retry */
+ if (cqr && cqr->collision) {
+ cqr->collision = false;
+ return true;
+ }
spin_lock_irqsave(&block->format_lock, flags);
- if (cqr->trkcount != atomic_read(&block->trkcount)) {
- /*
- * The number of formatted tracks has changed after request
- * start and we can not tell if the current track was involved.
- * To avoid data corruption treat it as if the current track is
- * involved
- */
+ while (i < DASD_NR_FORMAT_ENTRIES && device->format_entry[i].cqr)
+ i++;
+
+ if (i >= DASD_NR_FORMAT_ENTRIES) {
rc = true;
goto out;
}
+
list_for_each_entry(format, &block->format_list, list) {
- if (format->track == to_format->track) {
+ if (!(end < format->start_trk || format->end_trk < start)) {
rc = true;
goto out;
}
}
+ to_format = &device->format_entry[i];
+ to_format->start_trk = start;
+ to_format->end_trk = end;
+ to_format->cqr = cqr;
list_add_tail(&to_format->list, &block->format_list);
+ *entry = to_format;
out:
spin_unlock_irqrestore(&block->format_lock, flags);
@@ -3183,13 +3194,13 @@ static bool test_and_set_format_track(struct dasd_format_entry *to_format,
}
static void clear_format_track(struct dasd_format_entry *format,
- struct dasd_block *block)
+ struct dasd_block *block)
{
unsigned long flags;
spin_lock_irqsave(&block->format_lock, flags);
- atomic_inc(&block->trkcount);
list_del_init(&format->list);
+ format->cqr = NULL;
spin_unlock_irqrestore(&block->format_lock, flags);
}
@@ -3211,8 +3222,8 @@ static struct dasd_ccw_req *
dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
struct irb *irb)
{
+ struct dasd_format_entry *format = NULL;
struct dasd_eckd_private *private;
- struct dasd_format_entry *format;
struct format_data_t fdata;
unsigned int recs_per_trk;
struct dasd_ccw_req *fcqr;
@@ -3231,7 +3242,6 @@ dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
private = base->private;
blksize = block->bp_block;
recs_per_trk = recs_per_track(&private->rdc_data, 0, blksize);
- format = &startdev->format_entry;
first_trk = blk_rq_pos(req) >> block->s2b_shift;
sector_div(first_trk, recs_per_trk);
@@ -3248,9 +3258,9 @@ dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
curr_trk, first_trk, last_trk);
return ERR_PTR(-EINVAL);
}
- format->track = curr_trk;
+
/* test if track is already in formatting by another thread */
- if (test_and_set_format_track(format, cqr)) {
+ if (test_and_set_format_track(curr_trk, curr_trk, cqr, block, startdev, &format)) {
/* this is no real error so do not count down retries */
cqr->retries++;
return ERR_PTR(-EEXIST);
@@ -3262,17 +3272,27 @@ dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
fdata.intensity = private->uses_cdl ? DASD_FMT_INT_COMPAT : 0;
rc = dasd_eckd_format_sanity_checks(base, &fdata);
- if (rc)
+ if (rc) {
+ if (format)
+ clear_format_track(format, block);
return ERR_PTR(-EINVAL);
+ }
/*
* We're building the request with PAV disabled as we're reusing
* the former startdev.
*/
fcqr = dasd_eckd_build_format(base, startdev, &fdata, 0);
- if (IS_ERR(fcqr))
+ if (IS_ERR(fcqr)) {
+ if (format)
+ clear_format_track(format, block);
return fcqr;
+ }
+ if (format) {
+ format->cqr = fcqr;
+ fcqr->format = format;
+ }
fcqr->callback = dasd_eckd_ese_format_cb;
fcqr->callback_data = (void *) format;
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index 0ca039126ae9..a7010f24eb41 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -545,9 +545,17 @@ struct dasd_profile {
spinlock_t lock;
};
+/*
+ * concurrent ESE format ranges in flight; also caps a WRITE_FULL_TRACK's
+ * track count, which the LRE track bitmask limits to 16
+ */
+#define DASD_NR_FORMAT_ENTRIES 16
+
struct dasd_format_entry {
struct list_head list;
- sector_t track;
+ struct dasd_ccw_req *cqr;
+ sector_t start_trk;
+ sector_t end_trk;
};
struct dasd_device {
@@ -617,7 +625,7 @@ struct dasd_device {
struct dentry *debugfs_dentry;
struct dentry *hosts_dentry;
struct dasd_profile profile;
- struct dasd_format_entry format_entry;
+ struct dasd_format_entry format_entry[DASD_NR_FORMAT_ENTRIES];
struct kset *paths_info;
struct dasd_copy_relation *copy;
unsigned long aq_mask;
@@ -834,6 +842,13 @@ static inline void *dasd_get_callback_data(struct dasd_ccw_req *cqr)
return cqr->callback_data;
}
+static inline bool dasd_req_conflict(struct dasd_ccw_req *cqr1,
+ struct dasd_ccw_req *cqr2)
+{
+ return !(cqr1->format->end_trk < cqr2->start_trk ||
+ cqr2->end_trk < cqr1->format->start_trk);
+}
+
/* externals in dasd.c */
#define DASD_PROFILE_OFF 0
#define DASD_PROFILE_ON 1
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (7 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection Stefan Haberland
@ 2026-08-01 11:09 ` Stefan Haberland
2026-08-01 11:37 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack() Stefan Haberland
` (8 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:09 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
prepare_itcw() builds the FCX prefix block (PFX + LRE) for track-mode
I/O. Extend it to handle DASD_ECKD_CCW_WRITE_FULL_TRACK.
WRITE_FULL_TRACK needs two extra bytes appended to the LRE for that
bitmask. The prefix block is a scratch buffer copied into the TCCB by
itcw_add_dcw(), so keep it on the stack (sized for the two extra bytes)
rather than allocating it: this runs in the writeback path and must not
depend on an allocation that can fail under memory pressure.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 64 +++++++++++++++++++++++++++-------
1 file changed, 51 insertions(+), 13 deletions(-)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 577ce55da68f..a20ab98936b2 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -4383,11 +4383,13 @@ static int prepare_itcw(struct itcw *itcw,
unsigned int tlf,
unsigned int blk_per_trk)
{
- struct PFX_eckd_data pfxdata;
+ u8 pfxbuf[sizeof(struct PFX_eckd_data) + 2] __aligned(8);
+ struct PFX_eckd_data *pfxdata = (struct PFX_eckd_data *)pfxbuf;
struct dasd_eckd_private *basepriv, *startpriv;
struct DE_eckd_data *dedata;
struct LRE_eckd_data *lredata;
struct dcw *dcw;
+ int pfxsize;
u32 begcyl, endcyl;
u16 heads, beghead, endhead;
@@ -4397,26 +4399,31 @@ static int prepare_itcw(struct itcw *itcw,
int sector = 0;
int dn, d;
+ pfxsize = sizeof(struct PFX_eckd_data);
+ /* prefix + LRE extended data */
+ if (cmd == DASD_ECKD_CCW_WRITE_FULL_TRACK)
+ pfxsize += 2;
+
+ memset(pfxbuf, 0, pfxsize);
/* setup prefix data */
basepriv = basedev->private;
startpriv = startdev->private;
- dedata = &pfxdata.define_extent;
- lredata = &pfxdata.locate_record;
+ dedata = &pfxdata->define_extent;
+ lredata = &pfxdata->locate_record;
- memset(&pfxdata, 0, sizeof(pfxdata));
- pfxdata.format = 1; /* PFX with LRE */
- pfxdata.base_address = basepriv->conf.ned->unit_addr;
- pfxdata.base_lss = basepriv->conf.ned->ID;
- pfxdata.validity.define_extent = 1;
+ pfxdata->format = 1; /* PFX with LRE */
+ pfxdata->base_address = basepriv->conf.ned->unit_addr;
+ pfxdata->base_lss = basepriv->conf.ned->ID;
+ pfxdata->validity.define_extent = 1;
/* private uid is kept up to date, conf_data may be outdated */
if (startpriv->uid.type == UA_BASE_PAV_ALIAS)
- pfxdata.validity.verify_base = 1;
+ pfxdata->validity.verify_base = 1;
if (startpriv->uid.type == UA_HYPER_PAV_ALIAS) {
- pfxdata.validity.verify_base = 1;
- pfxdata.validity.hyper_pav = 1;
+ pfxdata->validity.verify_base = 1;
+ pfxdata->validity.hyper_pav = 1;
}
switch (cmd) {
@@ -4446,7 +4453,38 @@ static int prepare_itcw(struct itcw *itcw,
* data as well.
*/
if (dedata->ga_extended & 0x08 && dedata->ga_extended & 0x02)
- pfxdata.validity.time_stamp = 1; /* 'Time Stamp Valid' */
+ pfxdata->validity.time_stamp = 1; /* 'Time Stamp Valid' */
+ pfx_cmd = DASD_ECKD_CCW_PFX;
+ break;
+ case DASD_ECKD_CCW_WRITE_FULL_TRACK:
+ dedata->mask.perm = 0x3;
+ dedata->mask.auth = 0x00;
+ dedata->attributes.operation = basepriv->attrib.operation;
+ dedata->blk_size = blksize;
+ dedata->ga_extended |= 0x42;
+ rc = set_timestamp(NULL, dedata, basedev);
+ lredata->operation.orientation = 0x0;
+ lredata->operation.operation = 0x3F;
+ lredata->extended_operation = 0x11;
+ lredata->auxiliary.check_bytes = 0x2;
+ lredata->extended_parameter_length = 0x02;
+ if (count > 8) {
+ lredata->extended_parameter[0] = 0xFF;
+ lredata->extended_parameter[1] = 0xFF;
+ lredata->extended_parameter[1] <<= (16 - count);
+ } else {
+ lredata->extended_parameter[0] = 0xFF;
+ lredata->extended_parameter[0] <<= (8 - count);
+ lredata->extended_parameter[1] = 0x00;
+ }
+ sector = 0xFF;
+ /*
+ * If XRC is supported the System Time Stamp is set. The
+ * validity of the time stamp must be reflected in the prefix
+ * data as well.
+ */
+ if (dedata->ga_extended & 0x08 && dedata->ga_extended & 0x02)
+ pfxdata->validity.time_stamp = 1; /* 'Time Stamp Valid' */
pfx_cmd = DASD_ECKD_CCW_PFX;
break;
case DASD_ECKD_CCW_READ_COUNT_MT:
@@ -4525,7 +4563,7 @@ static int prepare_itcw(struct itcw *itcw,
lredata->search_arg.record = rec_on_trk;
dcw = itcw_add_dcw(itcw, pfx_cmd, 0,
- &pfxdata, sizeof(pfxdata), total_data_size);
+ pfxdata, pfxsize, total_data_size);
return PTR_ERR_OR_ZERO(dcw);
}
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack()
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (8 preceding siblings ...)
2026-08-01 11:09 ` [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:29 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler Stefan Haberland
` (7 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Add the channel program builder for WRITE_FULL_TRACK requests, used
by dasd_eckd_ese_format() (next patch) to format and write a set of
tracks atomically and avoid the format cycle on ESE devices.
The program is an ITCW with a TIDAW list. Per track it emits an eckd_r0
header, an eckd_count + data pair for every record (pad records before
and after the caller's data window use device->nulldata, records in the
window point into the bio payload), and a terminating 0xFF pseudo-count
with TIDAW_FLAGS_INSERT_CBC. The descriptors come from the per-device
fill_chunks pool so they can be freed in bulk in __dasd_cleanup_cqr().
Add inline helpers crosses_page() and reserve_nocross(), to keep each
descriptor within one page since TIDAW addressing must not cross a page
boundary.
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 342 +++++++++++++++++++++++++++++++++
1 file changed, 342 insertions(+)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index a20ab98936b2..27e91bd29092 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -123,6 +123,14 @@ static int prepare_itcw(struct itcw *, unsigned int, unsigned int, int,
unsigned int, unsigned int);
static int dasd_eckd_query_pprc_status(struct dasd_device *,
struct dasd_pprc_data_sc4 *);
+static struct dasd_ccw_req *dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *,
+ struct dasd_block *,
+ struct request *,
+ sector_t, sector_t,
+ sector_t, sector_t,
+ unsigned int, unsigned int,
+ unsigned int, unsigned int,
+ struct dasd_ccw_req *);
/* initial attempt at a probe function. this can be simplified once
* the other detection code is gone */
@@ -4740,6 +4748,340 @@ static struct dasd_ccw_req *dasd_eckd_build_cp_tpm_track(
return ERR_PTR(ret);
}
+static __always_inline bool crosses_page(const void *addr, size_t len)
+{
+ return len && (offset_in_page(addr) + len > PAGE_SIZE);
+}
+
+static __always_inline void *reserve_nocross(char **p, size_t *space, size_t len)
+{
+ size_t pad = crosses_page(*p, len) ? PAGE_SIZE - offset_in_page(*p) : 0;
+ void *ret;
+
+ if (*space < pad + len)
+ return NULL; /* out of space */
+
+ *p += pad;
+ *space -= pad;
+ ret = *p;
+ *p += len;
+ *space -= len;
+ return ret;
+}
+
+/*
+ * Helpers for dasd_eckd_build_cp_tpm_writefulltrack(): append the TIDAWs for
+ * one track-image element (R0 header, a count + data record, or the trailing
+ * pseudo track end count) to the itcw. Return the last TIDAW, or NULL on failure.
+ */
+static struct tidaw *add_track_r0(struct itcw *itcw, char **fill,
+ size_t *fillsize, u32 cyl, u16 head)
+{
+ struct tidaw *tidaw;
+ struct eckd_r0 *r0;
+
+ r0 = reserve_nocross(fill, fillsize, sizeof(*r0));
+ if (WARN_ON_ONCE(!r0))
+ return NULL;
+ set_chr_t(r0, cyl, head, 0);
+ r0->count.dl = 8;
+ tidaw = itcw_add_tidaw(itcw, 0, r0, sizeof(*r0));
+ return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
+}
+
+static struct tidaw *add_track_record(struct itcw *itcw, char **fill,
+ size_t *fillsize, u32 cyl, u16 head,
+ u8 rec, void *data, u32 dl)
+{
+ struct eckd_count *count;
+ struct tidaw *tidaw;
+
+ count = reserve_nocross(fill, fillsize, sizeof(*count));
+ if (WARN_ON_ONCE(!count))
+ return NULL;
+ set_chr_t(count, cyl, head, rec);
+ count->dl = dl;
+ tidaw = itcw_add_tidaw(itcw, 0, count, sizeof(*count));
+ if (IS_ERR_OR_NULL(tidaw))
+ return NULL;
+ tidaw = itcw_add_tidaw(itcw, 0, data, dl);
+ return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
+}
+
+static struct tidaw *add_track_end(struct itcw *itcw, char **fill,
+ size_t *fillsize)
+{
+ struct eckd_count *count;
+ struct tidaw *tidaw;
+
+ count = reserve_nocross(fill, fillsize, sizeof(*count));
+ if (WARN_ON_ONCE(!count))
+ return NULL;
+ count->cyl = 0xffff;
+ count->head = 0xffff;
+ count->dl = 0xffff;
+ count->record = 0xff;
+ count->kl = 0xff;
+ tidaw = itcw_add_tidaw(itcw, TIDAW_FLAGS_INSERT_CBC, count, sizeof(*count));
+ return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
+}
+
+static __maybe_unused struct dasd_ccw_req *
+dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *startdev,
+ struct dasd_block *block,
+ struct request *req,
+ sector_t first_rec,
+ sector_t last_rec,
+ sector_t first_trk,
+ sector_t last_trk,
+ unsigned int first_offs,
+ unsigned int last_offs,
+ unsigned int blk_per_trk,
+ unsigned int blksize,
+ struct dasd_ccw_req *ocqr)
+{
+ struct dasd_eckd_private *private = block->base->private;
+ unsigned int seg_len, part_len, len_to_track_end;
+ unsigned int count, count_to_trk_end, offs;
+ unsigned int trkcount, ctidaw, tlf;
+ int itcw_op, rec_count, datasize;
+ struct tidaw *last_tidaw = NULL;
+ sector_t recid, trkid, curr_trk;
+ unsigned char cmd, new_track;
+ struct dasd_device *basedev;
+ size_t itcw_size, fillsize;
+ struct dasd_ccw_req *cqr;
+ struct req_iterator iter;
+ char *dst, *filldata;
+ unsigned long flags;
+ struct itcw *itcw;
+ struct bio_vec bv;
+ int ret = -EINVAL;
+ void *nullrecord;
+ u16 heads, head;
+ u32 cyl;
+ u8 rec;
+
+ basedev = block->base;
+ cmd = DASD_ECKD_CCW_WRITE_FULL_TRACK;
+ itcw_op = ITCW_OP_WRITE;
+
+ /*
+ * trackbased I/O needs address all memory via TIDAWs,
+ * not just for 64 bit addresses. This allows us to map
+ * each segment directly to one tidaw.
+ * In the case of write requests, additional tidaws may
+ * be needed when a segment crosses a track boundary.
+ * Per track we emit one R0 tidaw, two tidaws per record (count field
+ * plus data - a record never crosses a track or page boundary, as
+ * part_len is clamped to both blksize and the track end), and one track
+ * end tidaw: 2 * blk_per_trk + 2.
+ * Round the +2 up to blk_per_trk-independent headroom via 2 * (blk_per_trk + 2).
+ */
+ trkcount = last_trk - first_trk + 1;
+ ctidaw = trkcount * 2 * (blk_per_trk + 2);
+
+ /*
+ * build_cp (ocqr == NULL): the request owns its CCW program - block in
+ * the pdu, ITCW in ccw_chunks. ese_format (ocqr != NULL): the failing
+ * origin still owns its pdu, so take the replacement from ese_chunks.
+ */
+ itcw_size = itcw_calc_size(0, ctidaw, 0);
+ if (ocqr)
+ cqr = dasd_fmalloc_request(DASD_ECKD_MAGIC, 0, itcw_size, startdev);
+ else
+ cqr = dasd_smalloc_request(DASD_ECKD_MAGIC, 0, itcw_size, startdev,
+ blk_mq_rq_to_pdu(req));
+ if (IS_ERR(cqr))
+ return cqr;
+ fillsize = trkcount * (sizeof(struct eckd_r0) +
+ (sizeof(struct eckd_count) * (blk_per_trk + 2)));
+ /*
+ * reserve_nocross() pads elements away from page boundaries and draws
+ * that padding from fillsize; budget one element per page the buffer
+ * may span so it never runs short.
+ */
+ fillsize += (fillsize / PAGE_SIZE + 1) * sizeof(struct eckd_r0);
+ spin_lock_irqsave(&startdev->mem_lock, flags);
+ filldata = dasd_alloc_chunk(&startdev->fill_chunks, fillsize);
+ spin_unlock_irqrestore(&startdev->mem_lock, flags);
+ if (!filldata) {
+ ret = -ENOMEM;
+ goto out_error;
+ }
+ memset(filldata, 0, fillsize);
+ cqr->filldata = filldata;
+
+ nullrecord = startdev->nulldata;
+
+ /* count + data for each record, plus r0 and the pseudo count */
+ tlf = blk_per_trk * (blksize + sizeof(struct eckd_count));
+ tlf += sizeof(struct eckd_r0) + sizeof(struct eckd_count);
+
+ itcw = itcw_init(cqr->data, itcw_size, itcw_op, 0, ctidaw, 0);
+ if (IS_ERR(itcw)) {
+ ret = -EINVAL;
+ goto out_error;
+ }
+ cqr->cpaddr = itcw_get_tcw(itcw);
+ datasize = trkcount * tlf;
+ if (prepare_itcw(itcw, first_trk, last_trk,
+ cmd, basedev, startdev,
+ 0,
+ trkcount, blksize,
+ datasize,
+ tlf,
+ blk_per_trk) == -EAGAIN) {
+ /* Clock not in sync and XRC is enabled.
+ * Try again later.
+ */
+ ret = -EAGAIN;
+ goto out_error;
+ }
+ heads = private->rdc_data.trk_per_cyl;
+ /*
+ * A tidaw can address 4k of memory, but must not cross page boundaries
+ * We can let the block layer handle this by setting seg_boundary_mask
+ * to page boundaries and max_segment_size to page size when setting up
+ * the request queue.
+ */
+ curr_trk = first_trk;
+ recid = first_rec;
+ trkid = recid;
+ offs = sector_div(trkid, blk_per_trk);
+ count = blk_per_trk;
+ len_to_track_end = count * blksize;
+ recid += count - first_offs;
+ new_track = 0;
+
+ /* the R0 header of the first track */
+ cyl = curr_trk / heads;
+ head = curr_trk % heads;
+ last_tidaw = add_track_r0(itcw, &filldata, &fillsize, cyl, head);
+ if (!last_tidaw)
+ goto out_error;
+
+ /* empty records before the first data record */
+ for (int i = 1; i <= first_offs; i++) {
+ len_to_track_end -= blksize;
+ last_tidaw = add_track_record(itcw, &filldata, &fillsize,
+ cyl, head, i, nullrecord, blksize);
+ if (!last_tidaw)
+ goto out_error;
+ }
+
+ /* process data records */
+ rec = first_offs + 1;
+ rec_count = 0;
+ rq_for_each_segment(bv, req, iter) {
+ dst = bvec_virt(&bv);
+ seg_len = bv.bv_len;
+ while (seg_len) {
+ if (new_track) {
+ trkid = recid;
+ offs = sector_div(trkid, blk_per_trk);
+ count_to_trk_end = blk_per_trk - offs;
+ count = min((last_rec - recid + 1),
+ (sector_t)count_to_trk_end);
+ /*
+ * Size to the physical track end: a short last
+ * track is padded in out_skip, so the track-end
+ * marker must not be emitted early here.
+ */
+ len_to_track_end = count_to_trk_end * blksize;
+ recid += count;
+ new_track = 0;
+ /* the R0 header of the next track */
+ cyl = curr_trk / heads;
+ head = curr_trk % heads;
+ last_tidaw = add_track_r0(itcw, &filldata,
+ &fillsize, cyl, head);
+ if (!last_tidaw)
+ goto out_error;
+ rec = 1;
+ }
+ /*
+ * One count + data record per block: a bvec segment can
+ * be up to a page, so clamp to blksize - otherwise the
+ * count field would describe one oversized record instead
+ * of several blksize ones for sub-page block sizes.
+ */
+ part_len = min(seg_len, len_to_track_end);
+ part_len = min(part_len, blksize);
+ seg_len -= part_len;
+ len_to_track_end -= part_len;
+ /*
+ * This block ends the track; the next one starts a new
+ * track. The track-end marker emitted below carries the
+ * CBC flag.
+ */
+ if (!len_to_track_end)
+ new_track = 1;
+
+ last_tidaw = add_track_record(itcw, &filldata, &fillsize,
+ cyl, head, rec, dst, part_len);
+ if (!last_tidaw)
+ goto out_error;
+
+ if (new_track) {
+ /* add track end marker */
+ last_tidaw = add_track_end(itcw, &filldata,
+ &fillsize);
+ if (!last_tidaw)
+ goto out_error;
+ curr_trk++;
+ }
+ rec++;
+ dst += part_len;
+ rec_count++;
+ if (rec_count >= (last_rec - first_rec + 1))
+ goto out_skip;
+ }
+ }
+
+out_skip:
+ new_track = 0;
+ /* empty records after the last data record */
+ for (int i = last_offs + 2; i <= blk_per_trk; i++) {
+ len_to_track_end -= blksize;
+ last_tidaw = add_track_record(itcw, &filldata, &fillsize,
+ cyl, head, i, nullrecord, blksize);
+ if (!last_tidaw)
+ goto out_error;
+ new_track = 1;
+ }
+
+ /* add track end marker */
+ if (new_track) {
+ last_tidaw = add_track_end(itcw, &filldata, &fillsize);
+ if (!last_tidaw)
+ goto out_error;
+ }
+
+ last_tidaw->flags |= TIDAW_FLAGS_LAST;
+ last_tidaw->flags &= ~TIDAW_FLAGS_INSERT_CBC;
+ itcw_finalize(itcw);
+
+ if (blk_noretry_request(req) ||
+ block->base->features & DASD_FEATURE_FAILFAST)
+ set_bit(DASD_CQR_FLAGS_FAILFAST, &cqr->flags);
+ cqr->cpmode = 1;
+ cqr->startdev = startdev;
+ cqr->memdev = startdev;
+ cqr->block = block;
+ cqr->expires = startdev->default_expires * HZ; /* default 5 minutes */
+ cqr->lpm = dasd_path_get_ppm(startdev);
+ cqr->retries = startdev->default_retries;
+ cqr->buildclk = get_tod_clock();
+ cqr->status = DASD_CQR_FILLED;
+
+ return cqr;
+out_error:
+ /* dasd_sfree_request frees from the right pool via cqr->mem_chunk */
+ dasd_sfree_request(cqr, startdev);
+ return ERR_PTR(ret);
+}
+
static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
struct dasd_block *block,
struct request *req)
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (9 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack() Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:39 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode Stefan Haberland
` (6 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Wire dasd_eckd_build_cp_tpm_writefulltrack() into the ESE unformated
track handler.
dasd_eckd_ese_format() now returns void (matching the revised discipline
hook): it computes the failing track/record range, trims a partially
covered last track when several tracks are involved (the block layer
re-issues the remainder), claims the range with
test_and_set_format_track(), builds a writefulltrack CQR, copies
callback_data/proc_bytes from the origin, and stages it on
block->ese_staging. The origin CQR is set to DASD_CQR_ABORT so
__dasd_process_cqr() retires it without the normal completion.
Drop dasd_eckd_ese_format_cb(); the format-entry slot is now released by
dasd_eckd_free_alias_cp() via clear_format_track() when the CQR is freed.
dasd_int_handler() calls the void hook directly and, for writefulltrack
CQRs (cqr->filldata set), returns DASD_CQR_ERROR instead of looping on
the NRF.
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd.c | 59 ++++++---
drivers/s390/block/dasd_3990_erp.c | 1 +
drivers/s390/block/dasd_eckd.c | 199 ++++++++++++++++++++---------
drivers/s390/block/dasd_erp.c | 8 +-
drivers/s390/block/dasd_int.h | 3 +-
5 files changed, 191 insertions(+), 79 deletions(-)
diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
index 9cb97df0866c..6a01d2140378 100644
--- a/drivers/s390/block/dasd.c
+++ b/drivers/s390/block/dasd.c
@@ -1618,7 +1618,7 @@ static int dasd_ese_oos_cond(u8 *sense)
void dasd_int_handler(struct ccw_device *cdev, unsigned long intparm,
struct irb *irb)
{
- struct dasd_ccw_req *cqr, *next, *fcqr;
+ struct dasd_ccw_req *cqr, *next;
struct dasd_device *device;
unsigned long now;
int nrf_suppressed = 0;
@@ -1740,26 +1740,23 @@ void dasd_int_handler(struct ccw_device *cdev, unsigned long intparm,
dasd_schedule_device_bh(device);
return;
}
- fcqr = device->discipline->ese_format(device, cqr, irb);
- if (IS_ERR(fcqr)) {
- if (PTR_ERR(fcqr) == -EINVAL) {
- cqr->status = DASD_CQR_ERROR;
- return;
- }
+ if (cqr->filldata) {
/*
- * If we can't format now, let the request go
- * one extra round. Maybe we can format later.
+ * A WRITE_FULL_TRACK cqr carries the complete
+ * track image; INV_TRACK_FORMAT here means the
+ * generated image or the media itself is bad, not
+ * that the track still needs formatting - retrying
+ * via ese_format() would just resubmit the same
+ * write. Let it fail instead.
*/
- cqr->status = DASD_CQR_QUEUED;
- dasd_schedule_device_bh(device);
- return;
- } else {
- fcqr->status = DASD_CQR_QUEUED;
- cqr->status = DASD_CQR_QUEUED;
- list_add(&fcqr->devlist, &device->ccw_queue);
+ cqr->status = DASD_CQR_ERROR;
+ cqr->stopclk = now;
+ dasd_device_clear_timer(device);
dasd_schedule_device_bh(device);
return;
}
+ device->discipline->ese_format(device, cqr, irb);
+ return;
}
/* Check for clear pending */
@@ -2726,6 +2723,13 @@ static void __dasd_process_erp(struct dasd_device *device,
if (cqr->status == DASD_CQR_DONE)
DBF_DEV_EVENT(DBF_NOTICE, device, "%s", "ERP successful");
+ else if (cqr->status == DASD_CQR_ABORTED)
+ /*
+ * ESE format aborts the request and replaces it with a format
+ * CQR - this is not an ERP failure.
+ */
+ DBF_DEV_EVENT(DBF_NOTICE, device, "%s",
+ "ERP request aborted, replaced by ESE format");
else
dev_err(&device->cdev->dev, "ERP failed for the DASD\n");
erp_fn = device->discipline->erp_postaction(cqr);
@@ -2766,6 +2770,9 @@ static void __dasd_cleanup_cqr(struct dasd_ccw_req *cqr)
error = BLK_STS_IOERR;
break;
}
+ } else if (status == DASD_CQR_ABORTED) {
+ /* aborted requests are replaced with a new one so do not complete this */
+ return;
}
/*
@@ -3172,6 +3179,13 @@ static blk_status_t do_dasd_request(struct blk_mq_hw_ctx *hctx,
}
goto out;
}
+ if (!cqr) {
+ /* build_cp may collapse a non-transient build error to NULL */
+ DBF_DEV_EVENT(DBF_ERR, basedev,
+ "CCW creation returned NULL on request %p", req);
+ rc = BLK_STS_IOERR;
+ goto out;
+ }
/*
* Note: callback is set to dasd_return_cqr_cb in
* __dasd_block_start_head to cover erp requests as well
@@ -3965,6 +3979,19 @@ int dasd_generic_requeue_all_requests(struct dasd_device *device)
*/
goto restart_cb;
}
+ /*
+ * An aborted request was replaced by a full-track write and is
+ * retired by that replacement; do not requeue it, just release
+ * it (mirrors the DASD_CQR_ABORTED handling in
+ * __dasd_cleanup_cqr()).
+ */
+ if (cqr->status == DASD_CQR_ABORTED) {
+ struct request *req = cqr->callback_data;
+
+ list_del_init(&cqr->blocklist);
+ cqr->block->base->discipline->free_cp(cqr, req);
+ continue;
+ }
_dasd_requeue_request(cqr);
list_del_init(&cqr->blocklist);
cqr->block->base->discipline->free_cp(
diff --git a/drivers/s390/block/dasd_3990_erp.c b/drivers/s390/block/dasd_3990_erp.c
index d0aa267462c5..736459477c19 100644
--- a/drivers/s390/block/dasd_3990_erp.c
+++ b/drivers/s390/block/dasd_3990_erp.c
@@ -2400,6 +2400,7 @@ static struct dasd_ccw_req *dasd_3990_erp_add_erp(struct dasd_ccw_req *cqr)
erp->startdev = device;
erp->memdev = device;
erp->block = cqr->block;
+ erp->filldata = cqr->filldata;
erp->magic = cqr->magic;
erp->expires = cqr->expires;
erp->retries = device->default_retries;
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 27e91bd29092..d757c6ebaea0 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -3212,36 +3212,24 @@ static void clear_format_track(struct dasd_format_entry *format,
spin_unlock_irqrestore(&block->format_lock, flags);
}
-/*
- * Callback function to free ESE format requests.
- */
-static void dasd_eckd_ese_format_cb(struct dasd_ccw_req *cqr, void *data)
-{
- struct dasd_device *device = cqr->startdev;
- struct dasd_eckd_private *private = device->private;
- struct dasd_format_entry *format = data;
-
- clear_format_track(format, cqr->basedev->block);
- private->count--;
- dasd_ffree_request(cqr, device);
-}
-
-static struct dasd_ccw_req *
-dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
- struct irb *irb)
+static void dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
+ struct irb *irb)
{
struct dasd_format_entry *format = NULL;
+ unsigned int first_offs, last_offs;
struct dasd_eckd_private *private;
- struct format_data_t fdata;
- unsigned int recs_per_trk;
+ struct dasd_ccw_req *base_cqr;
+ sector_t first_rec, last_rec;
+ sector_t first_trk, last_trk;
+ unsigned int proc_bytes = 0;
struct dasd_ccw_req *fcqr;
+ unsigned int recs_per_trk;
struct dasd_device *base;
struct dasd_block *block;
unsigned int blksize;
struct request *req;
- sector_t first_trk;
- sector_t last_trk;
sector_t curr_trk;
+ unsigned int diff;
int rc;
req = dasd_get_callback_data(cqr);
@@ -3251,60 +3239,138 @@ dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
blksize = block->bp_block;
recs_per_trk = recs_per_track(&private->rdc_data, 0, blksize);
- first_trk = blk_rq_pos(req) >> block->s2b_shift;
- sector_div(first_trk, recs_per_trk);
- last_trk =
- (blk_rq_pos(req) + blk_rq_sectors(req) - 1) >> block->s2b_shift;
- sector_div(last_trk, recs_per_trk);
- rc = dasd_eckd_track_from_irb(irb, base, &curr_trk);
- if (rc)
- return ERR_PTR(rc);
+ /* Calculate record id of first and last block. */
+ first_rec = blk_rq_pos(req) >> block->s2b_shift;
+ first_trk = first_rec;
+ first_offs = sector_div(first_trk, recs_per_trk);
+ last_rec = (blk_rq_pos(req) + blk_rq_sectors(req) - 1) >> block->s2b_shift;
+ last_trk = last_rec;
+ last_offs = sector_div(last_trk, recs_per_trk);
+ /*
+ * detect if some data has already been processed and the unformatted track is
+ * within the request.
+ * If so, finish the request first with the already processed bytes and let the
+ * blocklayer only redrive unformatted part.
+ * With this we ensure that there is no overlap of existing data with unformatted
+ * zero blocks
+ */
+ rc = dasd_eckd_track_from_irb(irb, base, &curr_trk);
+ if (rc) {
+ /* sense data could not be parsed - this will not resolve by retrying */
+ cqr->status = DASD_CQR_ERROR;
+ goto out;
+ }
+ if (curr_trk >= (sector_t)private->real_cyl * private->rdc_data.trk_per_cyl) {
+ DBF_DEV_EVENT(DBF_WARNING, startdev,
+ "ESE error track %llu exceeds device geometry\n",
+ curr_trk);
+ cqr->status = DASD_CQR_ERROR;
+ goto out;
+ }
if (curr_trk < first_trk || curr_trk > last_trk) {
DBF_DEV_EVENT(DBF_WARNING, startdev,
"ESE error track %llu not within range %llu - %llu\n",
curr_trk, first_trk, last_trk);
- return ERR_PTR(-EINVAL);
- }
-
- /* test if track is already in formatting by another thread */
- if (test_and_set_format_track(curr_trk, curr_trk, cqr, block, startdev, &format)) {
- /* this is no real error so do not count down retries */
- cqr->retries++;
- return ERR_PTR(-EEXIST);
+ cqr->status = DASD_CQR_ERROR;
+ goto out;
}
-
- fdata.start_unit = curr_trk;
- fdata.stop_unit = curr_trk;
- fdata.blksize = blksize;
- fdata.intensity = private->uses_cdl ? DASD_FMT_INT_COMPAT : 0;
-
- rc = dasd_eckd_format_sanity_checks(base, &fdata);
- if (rc) {
- if (format)
- clear_format_track(format, block);
- return ERR_PTR(-EINVAL);
+ if (curr_trk != first_trk) {
+ proc_bytes = ((curr_trk - first_trk) * recs_per_trk - first_offs) * blksize;
+ cqr->proc_bytes = proc_bytes;
+ cqr->status = DASD_CQR_SUCCESS;
+ cqr->stopclk = get_tod_clock();
+ goto out;
}
/*
- * We're building the request with PAV disabled as we're reusing
- * the former startdev.
+ * If there are multiple tracks to be format-written, we can not write
+ * the partial last track since we do not know if it is already formatted
+ * or not so skip the partial last track for now. Return the partial
+ * completion to blocklayer and let it redo the remainder
*/
- fcqr = dasd_eckd_build_format(base, startdev, &fdata, 0);
+ if (first_trk != last_trk && last_offs + 1 < recs_per_trk) {
+ diff = last_offs + 1;
+ last_rec = last_rec - diff;
+ last_trk = last_rec;
+ last_offs = sector_div(last_trk, recs_per_trk);
+ proc_bytes = (last_rec - first_rec + 1) * blksize;
+ }
+ if (first_offs > 0 || last_offs + 1 < recs_per_trk) {
+ /* test if tracks are already in formatting by another thread */
+ if (test_and_set_format_track(first_trk, last_trk, cqr,
+ cqr->block, cqr->startdev, &format)) {
+ /* this is no real error so do not count down retries */
+ cqr->retries++;
+ goto out_retry;
+ }
+ }
+
+ fcqr = dasd_eckd_build_cp_tpm_writefulltrack(startdev, block, req,
+ first_rec, last_rec,
+ first_trk, last_trk,
+ first_offs, last_offs,
+ recs_per_trk, blksize, cqr);
if (IS_ERR(fcqr)) {
if (format)
- clear_format_track(format, block);
- return fcqr;
+ clear_format_track(format, cqr->block);
+ if (PTR_ERR(fcqr) == -EINVAL) {
+ /* permanent build failure - fail instead of retrying */
+ cqr->status = DASD_CQR_ERROR;
+ goto out;
+ }
+ /*
+ * Transient conditions - the XRC clock is not in sync (-EAGAIN)
+ * or the format request pool is momentarily exhausted under load
+ * (-ENOMEM). Retry the origin without counting down its retries.
+ */
+ if (PTR_ERR(fcqr) == -EAGAIN || PTR_ERR(fcqr) == -ENOMEM)
+ cqr->retries++;
+ goto out_retry;
}
if (format) {
format->cqr = fcqr;
fcqr->format = format;
}
- fcqr->callback = dasd_eckd_ese_format_cb;
- fcqr->callback_data = (void *) format;
- return fcqr;
+ /*
+ * cqr may be an ERP request; dq and the owning request are only set on
+ * the base request at the end of the ERP chain, so copy from there.
+ */
+ base_cqr = cqr;
+ while (base_cqr->refers)
+ base_cqr = base_cqr->refers;
+ fcqr->dq = base_cqr->dq;
+ fcqr->callback_data = base_cqr->callback_data;
+ if (proc_bytes)
+ fcqr->proc_bytes = proc_bytes;
+ fcqr->status = DASD_CQR_FILLED;
+ ((struct dasd_eckd_private *)fcqr->memdev->private)->count++;
+ /*
+ * stage under ese_lock; dasd_block_tasklet splices it into ccw_queue.
+ * Direct enqueue here would invert queue_lock / ccwdev_lock.
+ */
+ spin_lock(&block->ese_lock);
+ list_add(&fcqr->blocklist, &block->ese_staging);
+ spin_unlock(&block->ese_lock);
+ /* mark origin CQR as aborted; ccwdev_lock is held by the IRQ handler */
+ cqr->status = DASD_CQR_ABORT;
+ goto out;
+
+out_retry:
+ /*
+ * If we can't format now, let the request go
+ * one extra round. Maybe we can format later.
+ * re-queue at the end to let potential format collision finish first
+ */
+ list_move_tail(&cqr->devlist, &cqr->startdev->ccw_queue);
+ cqr->status = DASD_CQR_QUEUED;
+out:
+ dasd_device_clear_timer(startdev);
+ dasd_schedule_block_bh(block);
+ dasd_schedule_device_bh(startdev);
+ return;
}
/*
@@ -4826,7 +4892,7 @@ static struct tidaw *add_track_end(struct itcw *itcw, char **fill,
return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
}
-static __maybe_unused struct dasd_ccw_req *
+static struct dasd_ccw_req *
dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *startdev,
struct dasd_block *block,
struct request *req,
@@ -5117,7 +5183,7 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
fcx_multitrack = private->features.feature[40] & 0x20;
data_size = blk_rq_bytes(req);
- if (data_size % blksize)
+ if (data_size % blksize || data_size == 0)
return ERR_PTR(-EINVAL);
/* tpm write request add CBC data on each track boundary */
if (rq_data_dir(req) == WRITE)
@@ -5159,6 +5225,11 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
first_trk, last_trk,
first_offs, last_offs,
blk_per_trk, blksize);
+
+ if (!IS_ERR(cqr)) {
+ cqr->start_trk = first_trk;
+ cqr->end_trk = last_trk;
+ }
return cqr;
}
@@ -5326,6 +5397,15 @@ dasd_eckd_free_cp(struct dasd_ccw_req *cqr, struct request *req)
sector_t recid;
int status;
+ /*
+ * A format-aborted request finished nothing - its replacement
+ * completes the block request - so report ABORTED instead of DONE,
+ * but still release its bounce buffers like any other request.
+ */
+ if (cqr->status == DASD_CQR_ABORTED)
+ status = DASD_CQR_ABORTED;
+ else
+ status = cqr->status == DASD_CQR_DONE;
if (!dasd_page_cache)
goto out;
private = cqr->block->base->private;
@@ -5361,7 +5441,6 @@ dasd_eckd_free_cp(struct dasd_ccw_req *cqr, struct request *req)
}
}
out:
- status = cqr->status == DASD_CQR_DONE;
dasd_sfree_request(cqr, cqr->memdev);
return status;
}
@@ -5438,6 +5517,8 @@ static int dasd_eckd_free_alias_cp(struct dasd_ccw_req *cqr,
private = cqr->memdev->private;
private->count--;
spin_unlock_irqrestore(get_ccwdev_lock(cqr->memdev->cdev), flags);
+ if (cqr->format)
+ clear_format_track(cqr->format, cqr->block);
return dasd_eckd_free_cp(cqr, req);
}
diff --git a/drivers/s390/block/dasd_erp.c b/drivers/s390/block/dasd_erp.c
index 468f0b2cc342..05d5366484d7 100644
--- a/drivers/s390/block/dasd_erp.c
+++ b/drivers/s390/block/dasd_erp.c
@@ -120,7 +120,7 @@ dasd_default_erp_action(struct dasd_ccw_req *cqr)
*/
struct dasd_ccw_req *dasd_default_erp_postaction(struct dasd_ccw_req *cqr)
{
- int success;
+ int success, aborted;
unsigned long startclk, stopclk;
struct dasd_device *startdev;
unsigned int proc_bytes;
@@ -128,6 +128,7 @@ struct dasd_ccw_req *dasd_default_erp_postaction(struct dasd_ccw_req *cqr)
BUG_ON(cqr->refers == NULL || cqr->function == NULL);
success = cqr->status == DASD_CQR_DONE;
+ aborted = cqr->status == DASD_CQR_ABORTED;
startclk = cqr->startclk;
stopclk = cqr->stopclk;
startdev = cqr->startdev;
@@ -150,7 +151,10 @@ struct dasd_ccw_req *dasd_default_erp_postaction(struct dasd_ccw_req *cqr)
cqr->stopclk = stopclk;
cqr->startdev = startdev;
cqr->proc_bytes = proc_bytes;
- if (success)
+ if (aborted)
+ /* base request is owned by the ESE format replacement CQR */
+ cqr->status = DASD_CQR_ABORTED;
+ else if (success)
cqr->status = DASD_CQR_DONE;
else {
cqr->status = DASD_CQR_FAILED;
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index a7010f24eb41..b523d5a08a17 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -413,8 +413,7 @@ struct dasd_discipline {
int (*ext_pool_warn_thrshld)(struct dasd_device *);
int (*ext_pool_oos)(struct dasd_device *);
int (*ext_pool_exhaust)(struct dasd_device *, struct dasd_ccw_req *);
- struct dasd_ccw_req *(*ese_format)(struct dasd_device *,
- struct dasd_ccw_req *, struct irb *);
+ void (*ese_format)(struct dasd_device *, struct dasd_ccw_req *, struct irb *);
int (*ese_read)(struct dasd_ccw_req *, struct irb *);
int (*pprc_status)(struct dasd_device *, struct dasd_pprc_data_sc4 *);
bool (*pprc_enabled)(struct dasd_device *);
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (10 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:27 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias Stefan Haberland
` (5 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Add a single per-device 'full_track_bias' sysfs attribute (0..100) that
gates the full-track write path. 0 disables it, 100 routes every aligned,
full-track write through dasd_eckd_build_cp_tpm_writefulltrack(). Values in
between are reserved for the adaptive heuristic added in the next patch.
For now any non-zero value simply enables full-track writes. Internally the
value is kept in the per-device 'ft_bias' field.
This will control the default IO path only.
In case we get an unformatted track error it will always be used to format
and write the track in one go.
The WRITE_FULL_TRACK command has an advantage on sparse formatted ESE
devices but it has an overall penalty for maximum throughput compared to
usual track based IO.
The attribute lives at /sys/bus/ccw/devices/<devid>/full_track_bias and
accepts 0..100. The default is DASD_FT_BIAS_DEFAULT; together with the
adaptive heuristic added in the next patch it uses full-track writes only
where they pay off, avoiding the ESE format penalty out of the box while
keeping the throughput cost off already-formatted volumes.
A 'full_track_bias' module parameter sets the initial value applied to
every device at online time; individual volumes can still be re-tuned
through their sysfs attribute afterwards.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_devmap.c | 39 ++++++++++++++++++++++++++++++++
drivers/s390/block/dasd_eckd.c | 36 +++++++++++++++++++++++++----
drivers/s390/block/dasd_int.h | 18 +++++++++++++++
3 files changed, 88 insertions(+), 5 deletions(-)
diff --git a/drivers/s390/block/dasd_devmap.c b/drivers/s390/block/dasd_devmap.c
index 381d616ad433..035c022255b6 100644
--- a/drivers/s390/block/dasd_devmap.c
+++ b/drivers/s390/block/dasd_devmap.c
@@ -1630,6 +1630,44 @@ dasd_expires_store(struct device *dev, struct device_attribute *attr,
static DEVICE_ATTR(expires, 0644, dasd_expires_show, dasd_expires_store);
+/* ESE fulltrack write aggressiveness knob (0..100, see DASD_FT_BIAS_*) */
+static ssize_t
+full_track_bias_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct dasd_device *device;
+ int len;
+
+ device = dasd_device_from_cdev(to_ccwdev(dev));
+ if (IS_ERR(device))
+ return -ENODEV;
+ len = sysfs_emit(buf, "%u\n", device->ft_bias);
+ dasd_put_device(device);
+ return len;
+}
+
+static ssize_t full_track_bias_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct dasd_device *device;
+ unsigned int val;
+
+ if (kstrtouint(buf, 0, &val) || val > DASD_FT_BIAS_MAX)
+ return -EINVAL;
+
+ device = dasd_device_from_cdev(to_ccwdev(dev));
+ if (IS_ERR(device))
+ return -ENODEV;
+
+ device->ft_bias = val;
+ device->fulltrack = val ? 1 : 0;
+
+ dasd_put_device(device);
+ return count;
+}
+
+static DEVICE_ATTR_RW(full_track_bias);
+
static ssize_t
dasd_retries_show(struct device *dev, struct device_attribute *attr, char *buf)
{
@@ -2425,6 +2463,7 @@ static struct attribute * dasd_attrs[] = {
&dev_attr_erplog.attr,
&dev_attr_failfast.attr,
&dev_attr_expires.attr,
+ &dev_attr_full_track_bias.attr,
&dev_attr_retries.attr,
&dev_attr_timeout.attr,
&dev_attr_reservation_policy.attr,
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index d757c6ebaea0..5347ebce7c2e 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -47,6 +47,18 @@
MODULE_DESCRIPTION("S/390 DASD ECKD Disks device driver");
MODULE_LICENSE("GPL");
+/*
+ * Default full-track write bias applied to every ESE volume at online time;
+ * individual volumes can be re-tuned afterwards through their per-device
+ * full_track_bias sysfs attribute. 0 disables full-track writes, 100 always
+ * uses them, 50 (the default) enables the adaptive heuristic. Values above
+ * DASD_FT_BIAS_MAX are capped when applied.
+ */
+static unsigned int full_track_bias = DASD_FT_BIAS_DEFAULT;
+module_param(full_track_bias, uint, 0644);
+MODULE_PARM_DESC(full_track_bias,
+ "Default ESE full-track write bias 0..100 (0=off, 1..99=adaptive, 100=always)");
+
static struct dasd_discipline dasd_eckd_discipline;
/* The ccw bus type uses this table to find devices that it sends to
@@ -2148,6 +2160,11 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
device->path_interval = DASD_ECKD_PATH_INTERVAL;
device->aq_timeouts = DASD_RETRIES_MAX;
+ /* default ESE fulltrack write aggressiveness from the module parameter */
+ device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
+ /* only the "always" endpoint forces fulltrack unconditionally here */
+ device->fulltrack = (device->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
+
if (private->conf.gneq) {
value = 1;
for (i = 0; i < private->conf.gneq->timeout.value; i++)
@@ -5199,11 +5216,20 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
/* do nothing, just fall through to the cmd mode single case */
} else if ((data_size <= private->fcx_max_data)
&& (fcx_multitrack || (first_trk == last_trk))) {
- cqr = dasd_eckd_build_cp_tpm_track(startdev, block, req,
- first_rec, last_rec,
- first_trk, last_trk,
- first_offs, last_offs,
- blk_per_trk, blksize);
+ if (!first_offs && (last_offs + 1 == blk_per_trk) &&
+ rq_data_dir(req) == WRITE && basedev->fulltrack) {
+ cqr = dasd_eckd_build_cp_tpm_writefulltrack(startdev, block, req,
+ first_rec, last_rec,
+ first_trk, last_trk,
+ first_offs, last_offs,
+ blk_per_trk, blksize, NULL);
+ } else {
+ cqr = dasd_eckd_build_cp_tpm_track(startdev, block, req,
+ first_rec, last_rec,
+ first_trk, last_trk,
+ first_offs, last_offs,
+ blk_per_trk, blksize);
+ }
if (IS_ERR(cqr) && (PTR_ERR(cqr) != -EAGAIN) &&
(PTR_ERR(cqr) != -ENOMEM))
cqr = NULL;
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index b523d5a08a17..e2df8666520f 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -629,6 +629,10 @@ struct dasd_device {
struct dasd_copy_relation *copy;
unsigned long aq_mask;
unsigned int aq_timeouts;
+
+ /* ESE fulltrack write control (see full_track_bias sysfs attribute) */
+ unsigned int ft_bias; /* aggressiveness 0..100: 0=off, 100=always */
+ unsigned int fulltrack; /* internal: use WRITE_FULL_TRACK for aligned writes */
};
struct dasd_block {
@@ -686,6 +690,20 @@ struct dasd_queue {
#define DASD_STOPPED_PPRC 32 /* PPRC swap */
#define DASD_STOPPED_NOSPC 128 /* no space left */
+/*
+ * ESE fulltrack write aggressiveness (full_track_bias sysfs attribute), 0..100:
+ * 0 - never use proactively WRITE_FULL_TRACK
+ * 100 - always use proactively WRITE_FULL_TRACK, no probing
+ * 1..99 - adaptive; higher means switch to ft more eagerly
+ * WRITE_FULL_TRACK has an advantage on sparse formatted ESE devices
+ * but it has an overall penalty for maximum throughput for fully
+ * formatted devices.
+ * The default of 50 tries to balance both and do some probing in between
+ * to choose the best mode for default IO.
+ */
+#define DASD_FT_BIAS_MAX 100
+#define DASD_FT_BIAS_DEFAULT 50
+
/* per device flags */
#define DASD_FLAG_OFFLINE 3 /* device is in offline processing */
#define DASD_FLAG_EER_SNSS 4 /* A SNSS is required */
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (11 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:46 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes Stefan Haberland
` (4 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Turn the middle of the ft_bias range (1..99) into an adaptive heuristic
that switches between fulltrack write (ft1) and plain write ft0 depending
on how sparse the device still is.
A sparse device benefits from fulltrack writes (it avoids the format/retry
cycle); once enough tracks are formatted the per-write overhead of
ft1 outweighs that. An state machine measures the NRF rate in short ft0
probe windows and flips back to ft1 when it is high
(FT1_ACTIVE -> PROBING -> FT0_STABLE, with a backing-off reprobe interval).
The four parameters are derived from ft_bias by linear interpolation,
anchored so ft_bias == 50 derives the following values:
ese_heu_start_interval - 2000 - IOs in ft1, before first ft0-Probe starts
ese_heu_probe_window - 100 - IOs in probe window
ese_heu_nrf_high - 10 ‰ (= 1 %) - TRACK_FORMAT rate that leads to ft1
ese_heu_max_interval - 500000 - Backoff-Cap: max. IOs between two probes
Higher is more eager to use ft1, and 0/100 skips the heuristic.
The NRF counter is bumped in dasd_eckd_ese_format() for both the classic
NRF sense and the HPF INV_TRACK_FORMAT equivalent.
The state machine resets to ft1 on check_characteristics, full format,
and release-space.
A read-only ese_heuristic_state sysfs attribute exposes the current mode.
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_devmap.c | 44 +++++++++++++++-
drivers/s390/block/dasd_eckd.c | 88 +++++++++++++++++++++++++++++++-
drivers/s390/block/dasd_int.h | 85 ++++++++++++++++++++++++++++++
3 files changed, 214 insertions(+), 3 deletions(-)
diff --git a/drivers/s390/block/dasd_devmap.c b/drivers/s390/block/dasd_devmap.c
index 035c022255b6..50301e545bbe 100644
--- a/drivers/s390/block/dasd_devmap.c
+++ b/drivers/s390/block/dasd_devmap.c
@@ -1659,8 +1659,14 @@ static ssize_t full_track_bias_store(struct device *dev,
if (IS_ERR(device))
return -ENODEV;
+ /*
+ * ft_bias is the tuning target; fulltrack is a best-effort mode hint
+ * that the per-IO heuristic also updates locklessly. A racing writer can
+ * at most leave a transient mismatch that self-corrects on the next IO,
+ * never corruption, so the update is left unlocked.
+ */
device->ft_bias = val;
- device->fulltrack = val ? 1 : 0;
+ dasd_ft_bias_apply(device);
dasd_put_device(device);
return count;
@@ -1668,6 +1674,41 @@ static ssize_t full_track_bias_store(struct device *dev,
static DEVICE_ATTR_RW(full_track_bias);
+static const char * const dasd_ese_heu_state_names[] = {
+ [DASD_ESE_HEU_FT1_ACTIVE] = "fulltrack active",
+ [DASD_ESE_HEU_PROBING] = "probing",
+ [DASD_ESE_HEU_FT0_STABLE] = "fulltrack inactive",
+};
+
+/* read-only: current full-track mode / adaptive FSM state, for observability */
+static ssize_t
+ese_heuristic_state_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct dasd_device *device;
+ unsigned int state;
+ int len;
+
+ device = dasd_device_from_cdev(to_ccwdev(dev));
+ if (IS_ERR(device))
+ return -ENODEV;
+ if (device->ft_bias == 0) {
+ len = sysfs_emit(buf, "fulltrack deactivated\n");
+ } else if (device->ft_bias >= DASD_FT_BIAS_MAX) {
+ len = sysfs_emit(buf, "fulltrack permanent active\n");
+ } else {
+ state = device->ese_probe_state;
+ if (state < ARRAY_SIZE(dasd_ese_heu_state_names))
+ len = sysfs_emit(buf, "%s\n", dasd_ese_heu_state_names[state]);
+ else
+ len = sysfs_emit(buf, "unknown\n");
+ }
+ dasd_put_device(device);
+ return len;
+}
+
+static DEVICE_ATTR_RO(ese_heuristic_state);
+
static ssize_t
dasd_retries_show(struct device *dev, struct device_attribute *attr, char *buf)
{
@@ -2464,6 +2505,7 @@ static struct attribute * dasd_attrs[] = {
&dev_attr_failfast.attr,
&dev_attr_expires.attr,
&dev_attr_full_track_bias.attr,
+ &dev_attr_ese_heuristic_state.attr,
&dev_attr_retries.attr,
&dev_attr_timeout.attr,
&dev_attr_reservation_policy.attr,
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 5347ebce7c2e..ac7f1c446d32 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -2162,8 +2162,7 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
/* default ESE fulltrack write aggressiveness from the module parameter */
device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
- /* only the "always" endpoint forces fulltrack unconditionally here */
- device->fulltrack = (device->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
+ dasd_ft_bias_apply(device);
if (private->conf.gneq) {
value = 1;
@@ -3171,6 +3170,13 @@ static int dasd_eckd_format_process_data(struct dasd_device *base,
static int dasd_eckd_format_device(struct dasd_device *base,
struct format_data_t *fdata, int enable_pav)
{
+ /*
+ * A full format (start_unit == 0) returns the device to a fully sparse
+ * state, so restart the heuristic from ft1 without an offline cycle.
+ */
+ if (fdata->start_unit == 0)
+ dasd_ft_bias_apply(base);
+
return dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
0, NULL);
}
@@ -3229,6 +3235,69 @@ static void clear_format_track(struct dasd_format_entry *format,
spin_unlock_irqrestore(&block->format_lock, flags);
}
+/*
+ * Adaptive ft_bias heuristic, called once per IO from dasd_eckd_build_cp().
+ * Probes the device formatting state by briefly switching to ft0 and measuring
+ * the NRF rate; parameters are derived from ft_bias.
+ */
+static void dasd_ese_heuristic_tick(struct dasd_device *basedev)
+{
+ int ios, nrf, rate;
+
+ if (atomic_inc_return(&basedev->ese_io_cnt) < (int)basedev->ese_probe_interval)
+ return;
+
+ /*
+ * One wins the race to evaluate, the rest see ios == 0 after the
+ * xchg and return early, preventing redundant state transitions.
+ */
+ ios = atomic_xchg(&basedev->ese_io_cnt, 0);
+ if (ios <= 0)
+ return;
+
+ switch (basedev->ese_probe_state) {
+ case DASD_ESE_HEU_FT1_ACTIVE:
+ /* Start ft0 probe window, reset NRF counter for clean measurement */
+ basedev->fulltrack = 0;
+ basedev->ese_probe_state = DASD_ESE_HEU_PROBING;
+ basedev->ese_probe_interval = basedev->ese_heu_probe_window;
+ atomic_set(&basedev->ese_nrf_window, 0);
+ break;
+
+ case DASD_ESE_HEU_PROBING:
+ case DASD_ESE_HEU_FT0_STABLE:
+ nrf = atomic_xchg(&basedev->ese_nrf_window, 0);
+ rate = (int)((u64)nrf * 1000 / ios);
+ if (rate > (int)basedev->ese_heu_nrf_high) {
+ /* NRF rate high: device still sparse, ft1 is better */
+ basedev->fulltrack = 1;
+ basedev->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
+ basedev->ese_probe_interval = basedev->ese_heu_start_interval;
+ } else if (basedev->ese_probe_state == DASD_ESE_HEU_PROBING) {
+ /*
+ * NRF rate low: device mostly formatted, ft0 is faster.
+ * Re-probe frequently at first, then back off below.
+ */
+ basedev->fulltrack = 0;
+ basedev->ese_probe_state = DASD_ESE_HEU_FT0_STABLE;
+ basedev->ese_probe_interval = basedev->ese_heu_probe_window;
+ } else {
+ /*
+ * Still stable in ft0: re-assert plain-write mode so a
+ * fulltrack value left behind by a racing sysfs write
+ * self-corrects, and back off the re-probe interval
+ * (double it, capped at max_interval) so a long-lived
+ * formatted device is not probed more often than needed.
+ */
+ basedev->fulltrack = 0;
+ basedev->ese_probe_interval =
+ min(basedev->ese_probe_interval * 2,
+ basedev->ese_heu_max_interval);
+ }
+ break;
+ }
+}
+
static void dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
struct irb *irb)
{
@@ -3253,6 +3322,8 @@ static void dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_r
block = cqr->block;
base = block->base;
private = base->private;
+ if (dasd_ese_adaptive(base))
+ atomic_inc(&base->ese_nrf_window);
blksize = block->bp_block;
recs_per_trk = recs_per_track(&private->rdc_data, 0, blksize);
@@ -4015,6 +4086,14 @@ static int dasd_eckd_release_space_full(struct dasd_device *device)
rc = dasd_sleep_on_interruptible(cqr);
+ if (!rc) {
+ /*
+ * Releasing all space (RAS) wipes every track and the device is fully
+ * sparse again, so restart the heuristic from ft1.
+ */
+ dasd_ft_bias_apply(device);
+ }
+
dasd_sfree_request(cqr, cqr->memdev);
return rc;
@@ -5183,6 +5262,11 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
struct dasd_ccw_req *cqr;
basedev = block->base;
+ if (dasd_ese_adaptive(basedev))
+ dasd_ese_heuristic_tick(basedev);
+ else
+ /* re-assert the endpoint mode: a stale heuristic write cannot stick */
+ basedev->fulltrack = (basedev->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
private = basedev->private;
/* Calculate number of blocks/records per track. */
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index e2df8666520f..c899073c19c8 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -633,6 +633,15 @@ struct dasd_device {
/* ESE fulltrack write control (see full_track_bias sysfs attribute) */
unsigned int ft_bias; /* aggressiveness 0..100: 0=off, 100=always */
unsigned int fulltrack; /* internal: use WRITE_FULL_TRACK for aligned writes */
+ /* adaptive heuristic (active for ft_bias 1..99), derived from ft_bias */
+ unsigned int ese_probe_state; /* heuristic FSM state */
+ unsigned int ese_probe_interval; /* IOs between evaluations */
+ atomic_t ese_io_cnt; /* IO counter for current window */
+ atomic_t ese_nrf_window; /* NRF/INV_TRACK_FORMAT events in window */
+ unsigned int ese_heu_start_interval; /* IOs before first probe */
+ unsigned int ese_heu_probe_window; /* IOs in probe window */
+ unsigned int ese_heu_max_interval; /* max IOs between probes (backoff cap) */
+ unsigned int ese_heu_nrf_high; /* NRF per-mille threshold → activate ft1 */
};
struct dasd_block {
@@ -704,6 +713,25 @@ struct dasd_queue {
#define DASD_FT_BIAS_MAX 100
#define DASD_FT_BIAS_DEFAULT 50
+/* ESE fulltrack heuristic FSM states (adaptive range, ft_bias 1..99) */
+#define DASD_ESE_HEU_FT1_ACTIVE 0 /* fulltrack write active */
+#define DASD_ESE_HEU_PROBING 1 /* ft0 probe window, measuring NRF rate */
+#define DASD_ESE_HEU_FT0_STABLE 2 /* device formatted, ft0 active */
+
+/*
+ * Heuristic parameters are derived from ft_bias by linear interpolation,
+ * anchored so that ft_bias == 50 reproduces the previously shipped defaults
+ * and ft_bias == 100 is the most aggressive end of the range.
+ * probe_window is constant.
+ */
+#define DASD_ESE_HEU_PROBE_WINDOW 100
+#define DASD_ESE_HEU_NRF_HIGH_A50 10 /* NRF per-mille threshold */
+#define DASD_ESE_HEU_NRF_HIGH_A100 1
+#define DASD_ESE_HEU_START_A50 2000 /* IOs before first probe */
+#define DASD_ESE_HEU_START_A100 500
+#define DASD_ESE_HEU_MAX_A50 500000 /* backoff cap */
+#define DASD_ESE_HEU_MAX_A100 20000
+
/* per device flags */
#define DASD_FLAG_OFFLINE 3 /* device is in offline processing */
#define DASD_FLAG_EER_SNSS 4 /* A SNSS is required */
@@ -866,6 +894,63 @@ static inline bool dasd_req_conflict(struct dasd_ccw_req *cqr1,
cqr2->end_trk < cqr1->format->start_trk);
}
+/*
+ * true when device is ese device and ft_bias selects the adaptive
+ * heuristic (neither hard endpoint)
+ */
+static inline bool dasd_ese_adaptive(struct dasd_device *device)
+{
+ return device->discipline &&
+ device->discipline->is_ese &&
+ device->discipline->is_ese(device) &&
+ device->ft_bias > 0 &&
+ device->ft_bias < DASD_FT_BIAS_MAX;
+}
+
+/*
+ * Linear interpolation of a heuristic parameter between its value at aggr==50
+ * (v50) and its value at aggr==100 (v100).
+ */
+static inline unsigned int dasd_ese_lerp(unsigned int v50, unsigned int v100,
+ unsigned int aggr)
+{
+ return (unsigned int)((int)v50 +
+ ((int)v100 - (int)v50) * ((int)aggr - 50) / 50);
+}
+
+/*
+ * Apply the ft_bias knob. For the hard endpoints just pin the mode; for the
+ * adaptive range derive the heuristic parameters from ft_bias and (re)start
+ * the FSM in ft1 so a freshly sparse device avoids the NRF penalty right away.
+ */
+static inline void dasd_ft_bias_apply(struct dasd_device *device)
+{
+ unsigned int a = device->ft_bias;
+
+ if (!dasd_ese_adaptive(device)) {
+ device->fulltrack = (a >= DASD_FT_BIAS_MAX) ? 1 : 0;
+ device->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
+ return;
+ }
+
+ device->ese_heu_nrf_high =
+ dasd_ese_lerp(DASD_ESE_HEU_NRF_HIGH_A50,
+ DASD_ESE_HEU_NRF_HIGH_A100, a);
+ device->ese_heu_start_interval =
+ dasd_ese_lerp(DASD_ESE_HEU_START_A50,
+ DASD_ESE_HEU_START_A100, a);
+ device->ese_heu_max_interval =
+ dasd_ese_lerp(DASD_ESE_HEU_MAX_A50,
+ DASD_ESE_HEU_MAX_A100, a);
+ device->ese_heu_probe_window = DASD_ESE_HEU_PROBE_WINDOW;
+
+ device->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
+ device->ese_probe_interval = device->ese_heu_start_interval;
+ device->fulltrack = 1;
+ atomic_set(&device->ese_io_cnt, 0);
+ atomic_set(&device->ese_nrf_window, 0);
+}
+
/* externals in dasd.c */
#define DASD_PROFILE_OFF 0
#define DASD_PROFILE_ON 1
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (12 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:33 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label Stefan Haberland
` (3 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
When a CDL volume is formatted, write a small on-disk label so the format
can later be recognised by the kernel. The next patch will use this for
ESE detection.
The label records a magic, a version, whether the volume is ESE, and
whether it was formatted quick (space released, thin) or full.
It lives in track 0, head 0, record 4 (the first non-special CDL record).
R4 is written by the same channel program that formats track 0
- its WRITE_CKD transfers count + the label data instead of count-only -
so label and track format reach the disk atomically; a valid magic then
marks a completed format without a separate, racy write.
Quick vs full is derived from a full space release (RAS) preceding the
format: dasd_eckd_release_space_full() sets a per-device flag the next
format consumes. Non-ESE volumes and formats without a preceding full
release are recorded as full.
struct dasd_format_label is exactly 512 bytes (the smallest block size)
so it fits one record; larger blocks zero-pad the rest.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 81 +++++++++++++++++++++++++++++++---
drivers/s390/block/dasd_eckd.h | 38 ++++++++++++++++
2 files changed, 114 insertions(+), 5 deletions(-)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index ac7f1c446d32..b09ea243a955 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -19,6 +19,7 @@
#include <linux/init.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>
+#include <linux/utsname.h>
#include <linux/io.h>
#include <asm/css_chars.h>
@@ -2719,6 +2720,28 @@ dasd_eckd_build_check(struct dasd_device *base, struct format_data_t *fdata,
return cqr;
}
+/* Fill the format label into a R4 record buffer, zero-padded to blksize. */
+static void dasd_eckd_fill_format_label(struct dasd_device *device, void *data,
+ unsigned int blksize)
+{
+ struct dasd_eckd_private *private = device->private;
+ struct dasd_format_label *label = data;
+
+ memset(label, 0, blksize);
+ label->magic = DASD_ESE_LABEL_MAGIC;
+ label->version = DASD_ESE_LABEL_VERSION;
+ if (dasd_eckd_is_ese(device))
+ label->flags |= DASD_ESE_LABEL_F_ESE;
+ if (private->ese_format_quick)
+ label->flags |= DASD_ESE_LABEL_F_QUICK;
+ else
+ label->flags |= DASD_ESE_LABEL_F_FULL;
+ label->blksize = blksize;
+ label->format_tod = get_tod_clock();
+ strscpy(label->kernel_version, init_utsname()->release,
+ sizeof(label->kernel_version));
+}
+
static struct dasd_ccw_req *
dasd_eckd_build_format(struct dasd_device *base, struct dasd_device *startdev,
struct format_data_t *fdata, int enable_pav)
@@ -2737,6 +2760,7 @@ dasd_eckd_build_format(struct dasd_device *base, struct dasd_device *startdev,
int r0_perm;
int nr_tracks;
int use_prefix;
+ int write_label;
if (enable_pav)
startdev = dasd_alias_get_start_dev(base);
@@ -2770,6 +2794,15 @@ dasd_eckd_build_format(struct dasd_device *base, struct dasd_device *startdev,
use_prefix = base_priv->features.feature[8] & 0x01;
+ /*
+ * Stamp the format label into R4 of the very first track. Only for CDL
+ * (R4 is the first non-special record there), only when this request
+ * covers track 0, only for the record-writing format intensities (not
+ * track invalidation), and only if the track actually has an R4.
+ */
+ write_label = (intensity & 0x08) && !((intensity & ~0x08) & 0x04) &&
+ fdata->start_unit == 0 && rpt > 3;
+
switch (intensity) {
case 0x00: /* Normal format */
case 0x08: /* Normal format, use cdl. */
@@ -2816,6 +2849,10 @@ dasd_eckd_build_format(struct dasd_device *base, struct dasd_device *startdev,
return ERR_PTR(-EINVAL);
}
+ /* room for the label data that R4 carries in addition to its count */
+ if (write_label)
+ datasize += fdata->blksize;
+
fcp = dasd_fmalloc_request(DASD_ECKD_MAGIC, cplength, datasize, startdev);
if (IS_ERR(fcp))
return fcp;
@@ -2960,7 +2997,21 @@ dasd_eckd_build_format(struct dasd_device *base, struct dasd_device *startdev,
ccw->cmd_code =
DASD_ECKD_CCW_WRITE_CKD_MT;
ccw->flags = CCW_FLAG_SLI;
- ccw->count = 8;
+ if (write_label && address.cyl == 0 &&
+ address.head == 0 && i == 3) {
+ /*
+ * R4 carries the label as its record
+ * data; it follows ect contiguously so
+ * the CCW transfers count + data.
+ */
+ dasd_eckd_fill_format_label(base,
+ data,
+ fdata->blksize);
+ data += fdata->blksize;
+ ccw->count = 8 + fdata->blksize;
+ } else {
+ ccw->count = 8;
+ }
ccw->cda = virt_to_dma32(ect);
ccw++;
}
@@ -3170,6 +3221,9 @@ static int dasd_eckd_format_process_data(struct dasd_device *base,
static int dasd_eckd_format_device(struct dasd_device *base,
struct format_data_t *fdata, int enable_pav)
{
+ struct dasd_eckd_private *private = base->private;
+ int rc;
+
/*
* A full format (start_unit == 0) returns the device to a fully sparse
* state, so restart the heuristic from ft1 without an offline cycle.
@@ -3177,8 +3231,18 @@ static int dasd_eckd_format_device(struct dasd_device *base,
if (fdata->start_unit == 0)
dasd_ft_bias_apply(base);
- return dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
- 0, NULL);
+ rc = dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
+ 0, NULL);
+
+ /*
+ * The quick-format indicator was consumed by the label stamped into
+ * track 0; clear it so a later format that is not preceded by a full
+ * space release is recorded as a full format.
+ */
+ if (fdata->start_unit == 0)
+ private->ese_format_quick = 0;
+
+ return rc;
}
static bool test_and_set_format_track(sector_t start, sector_t end,
@@ -4077,6 +4141,7 @@ dasd_eckd_dso_ras(struct dasd_device *device, struct dasd_block *block,
static int dasd_eckd_release_space_full(struct dasd_device *device)
{
+ struct dasd_eckd_private *private;
struct dasd_ccw_req *cqr;
int rc;
@@ -4088,10 +4153,16 @@ static int dasd_eckd_release_space_full(struct dasd_device *device)
if (!rc) {
/*
- * Releasing all space (RAS) wipes every track and the device is fully
- * sparse again, so restart the heuristic from ft1.
+ * Releasing all space (RAS) wipes every track and the device is
+ * fully sparse again, so restart the heuristic from ft1.
*/
dasd_ft_bias_apply(device);
+ /*
+ * A full release is what makes a subsequent format a quick
+ * (thin) one; remember it so the format label records that.
+ */
+ private = device->private;
+ private->ese_format_quick = 1;
}
dasd_sfree_request(cqr, cqr->memdev);
diff --git a/drivers/s390/block/dasd_eckd.h b/drivers/s390/block/dasd_eckd.h
index 0fdb92fdddc8..92fd8ac92b79 100644
--- a/drivers/s390/block/dasd_eckd.h
+++ b/drivers/s390/block/dasd_eckd.h
@@ -159,6 +159,39 @@ struct eckd_r0 {
#define DASD_EAV_CYL_HI_SHIFT 16 /* cylinder bits beyond the 16-bit cyl field */
#define DASD_EAV_HEAD_HI_SHIFT 4 /* head occupies the low-order 4 bits of head */
+/*
+ * On-disk DASD format label.
+ *
+ * Written into track 0, head 0, record 4 (R4 - the first non-special CDL
+ * record) as part of the same channel program that formats track 0, so it is
+ * stored atomically with the track: either both the track format and the label
+ * make it to disk or neither does. Its presence with a valid magic therefore
+ * marks a completed format and can be used for format detection.
+ *
+ * The structure is exactly the smallest supported block size (512 bytes) so it
+ * always fits into a single record.
+ * For larger block sizes the rest of the record is zero padded.
+ * The magic together with the version is used to recognise a valid label.
+ */
+#define DASD_ESE_LABEL_MAGIC 0xC4C1E2C4C6D4E3F1ULL /* EBCDIC "DASDFMT1" */
+#define DASD_ESE_LABEL_VERSION 1
+
+/* dasd_format_label.flags */
+#define DASD_ESE_LABEL_F_ESE 0x00000001 /* volume is extent space efficient */
+#define DASD_ESE_LABEL_F_QUICK 0x00000002 /* quick (space released) format */
+#define DASD_ESE_LABEL_F_FULL 0x00000004 /* full format */
+
+struct dasd_format_label {
+ __u64 magic; /* DASD_ESE_LABEL_MAGIC */
+ __u32 version; /* DASD_ESE_LABEL_VERSION */
+ __u32 flags; /* DASD_ESE_LABEL_F_* */
+ __u32 blksize; /* block size the volume was formatted with */
+ __u32 reserved0;
+ __u64 format_tod; /* TOD clock at format time */
+ __u8 kernel_version[64]; /* NUL terminated kernel release (uname -r) */
+ __u8 reserved[416]; /* pad the struct to 512 bytes */
+} __packed;
+
struct ch_t {
__u16 cyl;
__u16 head;
@@ -709,6 +742,11 @@ struct dasd_eckd_private {
u32 fcx_max_data;
char suc_reason;
+ /*
+ * Set when the whole volume's space was released (full RAS); consumed by
+ * the next format to mark the on-disk label as a quick (vs full) format.
+ */
+ int ese_format_quick;
};
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (13 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:38 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online Stefan Haberland
` (2 subsequent siblings)
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Read the format label from track 0 record 4 at device bring-up and cache
it. When a valid label is present, is_ese() is derived from it instead of
the hardware volume field.
A volume copied off ESE storage onto other hardware is thus still handled
as thin.
Without a label (older format) is_ese() falls back to the hardware field as
before.
The cache is refreshed after a format so is_ese() stays coherent without an
offline/online cycle.
The label F_ESE bit is stamped from the hardware capability rather than
is_ese(), and space release (quick format) is gated on the hardware
capability, so a copied label cannot enable it on non-ESE hardware.
The ese sysfs attribute, and with this lsdasd, shows the hardware
capability and not the internal handling. This is in line with the view
from storage server interface. To reflect the specific internal handling an
additional attribute on_demand_formatting is added to show that a device is
handled like an ESE device internally based on the disk label.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_devmap.c | 6 +-
drivers/s390/block/dasd_eckd.c | 181 ++++++++++++++++++++++++++++---
drivers/s390/block/dasd_eckd.h | 7 ++
drivers/s390/block/dasd_int.h | 3 +
4 files changed, 180 insertions(+), 17 deletions(-)
diff --git a/drivers/s390/block/dasd_devmap.c b/drivers/s390/block/dasd_devmap.c
index 50301e545bbe..89f155e414fd 100644
--- a/drivers/s390/block/dasd_devmap.c
+++ b/drivers/s390/block/dasd_devmap.c
@@ -2479,9 +2479,10 @@ static ssize_t dasd_##_name##_show(struct device *dev, \
\
return sysfs_emit(buf, "%d\n", val); \
} \
-static DEVICE_ATTR(_name, 0444, dasd_##_name##_show, NULL); \
+static DEVICE_ATTR(_name, 0444, dasd_##_name##_show, NULL);
-DASD_DEFINE_ATTR(ese, device->discipline->is_ese);
+DASD_DEFINE_ATTR(ese, device->discipline->ese_capable);
+DASD_DEFINE_ATTR(on_demand_formatting, device->discipline->on_demand_format);
DASD_DEFINE_ATTR(extent_size, device->discipline->ext_size);
DASD_DEFINE_ATTR(pool_id, device->discipline->ext_pool_id);
DASD_DEFINE_ATTR(space_configured, device->discipline->space_configured);
@@ -2519,6 +2520,7 @@ static struct attribute * dasd_attrs[] = {
&dev_attr_path_reset.attr,
&dev_attr_hpf.attr,
&dev_attr_ese.attr,
+ &dev_attr_on_demand_formatting.attr,
&dev_attr_fc_security.attr,
&dev_attr_copy_pair.attr,
&dev_attr_copy_role.attr,
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index b09ea243a955..69a6a462e3e2 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -1676,7 +1676,8 @@ static int dasd_eckd_read_vol_info(struct dasd_device *device)
return rc;
}
-static int dasd_eckd_is_ese(struct dasd_device *device)
+/* Hardware/volume ESE capability, from the Volume Storage Query. */
+static int dasd_eckd_ese_capable(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
@@ -1686,6 +1687,53 @@ static int dasd_eckd_is_ese(struct dasd_device *device)
return private->vsq.vol_info.ese;
}
+/*
+ * Whether the volume is to be handled as ESE (thin). This reflects the state
+ * of the data, not the hardware: a volume copied off ESE storage onto other
+ * hardware still needs ESE handling. The on-disk format label is authoritative
+ * when present; without it (e.g. a volume formatted by an older driver) fall
+ * back to the hardware ESE field.
+ *
+ * Only the F_ESE flag gates this. An ESE volume is thin regardless of whether
+ * it was quick- or full-formatted (tracks are allocated on write, and discard
+ * re-thins a full one).
+ */
+static int dasd_eckd_is_ese(struct dasd_device *device)
+{
+ struct dasd_eckd_private *private = device->private;
+
+ /* sysfs may read this during set_online before private is allocated */
+ if (!private)
+ return 0;
+
+ if (private->ese_label_valid)
+ return !!(private->ese_label.flags & DASD_ESE_LABEL_F_ESE);
+
+ return dasd_eckd_ese_capable(device);
+}
+
+/*
+ * Whether the volume is formatted on demand (thin), as opposed to fully
+ * formatted. This is the format mode, not the hardware ESE capability. When a
+ * label is present it is authoritative (F_QUICK). Without a label the mode is
+ * unknown, but an ESE volume is still handled on demand (NRF triggers the
+ * format), so fall back to the ESE state to stay consistent with the driver's
+ * behavior on older, label-less volumes.
+ */
+static int dasd_eckd_on_demand_format(struct dasd_device *device)
+{
+ struct dasd_eckd_private *private = device->private;
+
+ /* sysfs may read this during set_online before private is allocated */
+ if (!private)
+ return 0;
+
+ if (private->ese_label_valid)
+ return !!(private->ese_label.flags & DASD_ESE_LABEL_F_QUICK);
+
+ return dasd_eckd_is_ese(device);
+}
+
static int dasd_eckd_ext_pool_id(struct dasd_device *device)
{
struct dasd_eckd_private *private = device->private;
@@ -2105,6 +2153,69 @@ static bool dasd_eckd_pprc_enabled(struct dasd_device *device)
return private->rdc_data.facilities.PPRC_enabled;
}
+/*
+ * Read the on-disk format label from track 0, record 4. On a formatted volume
+ * R4 holds the label as its record data; on an unformatted (fresh ESE) or
+ * label-less volume the read returns No Record Found, which is expected and
+ * leaves the cache invalid so is_ese() falls back to the hardware field.
+ */
+static void dasd_eckd_read_format_label(struct dasd_device *device)
+{
+ struct dasd_eckd_private *private = device->private;
+ struct dasd_format_label *label;
+ struct DE_eckd_data *dedata;
+ struct LO_eckd_data *lodata;
+ struct dasd_ccw_req *cqr;
+ struct ccw1 *ccw;
+
+ private->ese_label_valid = false;
+
+ /* The label lives on the base volume; aliases have none of their own. */
+ if (private->uid.type == UA_BASE_PAV_ALIAS ||
+ private->uid.type == UA_HYPER_PAV_ALIAS)
+ return;
+
+ cqr = dasd_smalloc_request(DASD_ECKD_MAGIC, 3 /* DE + LO + READ */,
+ sizeof(*dedata) + sizeof(*lodata) +
+ sizeof(*label), device, NULL);
+ if (IS_ERR(cqr))
+ return;
+
+ dedata = cqr->data;
+ lodata = (struct LO_eckd_data *)(dedata + 1);
+ label = (struct dasd_format_label *)(lodata + 1);
+
+ ccw = cqr->cpaddr;
+ define_extent(ccw++, dedata, 0, 0, DASD_ECKD_CCW_READ, device, 0);
+ ccw[-1].flags |= CCW_FLAG_CC;
+ locate_record(ccw++, lodata, 0, 4, 1, DASD_ECKD_CCW_READ, device,
+ sizeof(*label));
+ ccw[-1].flags |= CCW_FLAG_CC;
+ ccw->cmd_code = DASD_ECKD_CCW_READ;
+ ccw->count = sizeof(*label);
+ ccw->flags = CCW_FLAG_SLI;
+ ccw->cda = virt_to_dma32(label);
+
+ cqr->startdev = device;
+ cqr->memdev = device;
+ cqr->block = NULL;
+ cqr->retries = 256;
+ cqr->expires = 10 * HZ;
+ cqr->buildclk = get_tod_clock();
+ cqr->status = DASD_CQR_FILLED;
+ /* R4 may be absent (unformatted) or larger than the label. */
+ set_bit(DASD_CQR_SUPPRESS_NRF, &cqr->flags);
+ set_bit(DASD_CQR_SUPPRESS_IL, &cqr->flags);
+
+ if (!dasd_sleep_on(cqr) &&
+ label->magic == DASD_ESE_LABEL_MAGIC &&
+ label->version == DASD_ESE_LABEL_VERSION) {
+ private->ese_label = *label;
+ private->ese_label_valid = true;
+ }
+ dasd_sfree_request(cqr, device);
+}
+
/*
* Check device characteristics.
* If the device is accessible using ECKD discipline, the device is enabled.
@@ -2161,10 +2272,6 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
device->path_interval = DASD_ECKD_PATH_INTERVAL;
device->aq_timeouts = DASD_RETRIES_MAX;
- /* default ESE fulltrack write aggressiveness from the module parameter */
- device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
- dasd_ft_bias_apply(device);
-
if (private->conf.gneq) {
value = 1;
for (i = 0; i < private->conf.gneq->timeout.value; i++)
@@ -2219,6 +2326,16 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
/* Read Volume Information */
dasd_eckd_read_vol_info(device);
+ /* Read the on-disk format label for ESE detection */
+ dasd_eckd_read_format_label(device);
+
+ /*
+ * is_ese() now reflects the real ESE state (vsq + on-disk label), so
+ * the adaptive heuristic can be derived correctly for this device.
+ */
+ device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
+ dasd_ft_bias_apply(device);
+
/* Read Extent Pool Information */
dasd_eckd_read_ext_pool_info(device);
@@ -2730,7 +2847,12 @@ static void dasd_eckd_fill_format_label(struct dasd_device *device, void *data,
memset(label, 0, blksize);
label->magic = DASD_ESE_LABEL_MAGIC;
label->version = DASD_ESE_LABEL_VERSION;
- if (dasd_eckd_is_ese(device))
+ /*
+ * F_ESE records the hardware capability at format time, not is_ese():
+ * is_ese() is derived from the label, so using it here would let the
+ * flag flip on repeated quick/full reformats.
+ */
+ if (dasd_eckd_ese_capable(device))
label->flags |= DASD_ESE_LABEL_F_ESE;
if (private->ese_format_quick)
label->flags |= DASD_ESE_LABEL_F_QUICK;
@@ -2740,6 +2862,13 @@ static void dasd_eckd_fill_format_label(struct dasd_device *device, void *data,
label->format_tod = get_tod_clock();
strscpy(label->kernel_version, init_utsname()->release,
sizeof(label->kernel_version));
+
+ /*
+ * Populate the cache directly from the bytes just computed instead of
+ * synchronously reading them back from disk after the write lands.
+ */
+ private->ese_label = *label;
+ private->ese_label_valid = true;
}
static struct dasd_ccw_req *
@@ -3224,23 +3353,35 @@ static int dasd_eckd_format_device(struct dasd_device *base,
struct dasd_eckd_private *private = base->private;
int rc;
- /*
- * A full format (start_unit == 0) returns the device to a fully sparse
- * state, so restart the heuristic from ft1 without an offline cycle.
- */
- if (fdata->start_unit == 0)
- dasd_ft_bias_apply(base);
-
rc = dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
0, NULL);
+ if (fdata->start_unit != 0)
+ return rc;
+
+ if (rc) {
+ /*
+ * The format failed, so the label cached speculatively during
+ * CCW build may not match the disk; drop it so is_ese() falls
+ * back to the hardware field until the next successful format
+ * or bring-up.
+ */
+ private->ese_label_valid = false;
+ return rc;
+ }
/*
* The quick-format indicator was consumed by the label stamped into
* track 0; clear it so a later format that is not preceded by a full
* space release is recorded as a full format.
*/
- if (fdata->start_unit == 0)
- private->ese_format_quick = 0;
+ private->ese_format_quick = 0;
+
+ /*
+ * A full format returns the device to a fully sparse state and has just
+ * committed a fresh label; restart the heuristic from ft1 on the now
+ * current is_ese state, without an offline cycle.
+ */
+ dasd_ft_bias_apply(base);
return rc;
}
@@ -4241,6 +4382,14 @@ static int dasd_eckd_release_space_trks(struct dasd_device *device,
static int dasd_eckd_release_space(struct dasd_device *device,
struct format_data_t *rdata)
{
+ /*
+ * Space release (and thus a quick format) requires real ESE hardware.
+ * is_ese() may be true from a copied label on non-ESE hardware, so gate
+ * on the hardware capability, not on is_ese().
+ */
+ if (!dasd_eckd_ese_capable(device))
+ return -EOPNOTSUPP;
+
if (rdata->intensity & DASD_FMT_INT_ESE_FULL)
return dasd_eckd_release_space_full(device);
else if (rdata->intensity == 0)
@@ -7613,6 +7762,8 @@ static struct dasd_discipline dasd_eckd_discipline = {
.hpf_enabled = dasd_eckd_hpf_enabled,
.reset_path = dasd_eckd_reset_path,
.is_ese = dasd_eckd_is_ese,
+ .ese_capable = dasd_eckd_ese_capable,
+ .on_demand_format = dasd_eckd_on_demand_format,
.space_allocated = dasd_eckd_space_allocated,
.space_configured = dasd_eckd_space_configured,
.logical_capacity = dasd_eckd_logical_capacity,
diff --git a/drivers/s390/block/dasd_eckd.h b/drivers/s390/block/dasd_eckd.h
index 92fd8ac92b79..30745f62402b 100644
--- a/drivers/s390/block/dasd_eckd.h
+++ b/drivers/s390/block/dasd_eckd.h
@@ -747,6 +747,13 @@ struct dasd_eckd_private {
* the next format to mark the on-disk label as a quick (vs full) format.
*/
int ese_format_quick;
+ /*
+ * Cached on-disk format label (R4), read at online and refreshed on
+ * format. When valid, is_ese() is derived from it; otherwise it falls
+ * back to the hardware ESE field (vsq.vol_info.ese).
+ */
+ struct dasd_format_label ese_label;
+ bool ese_label_valid;
};
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index c899073c19c8..f61af45deab0 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -401,6 +401,9 @@ struct dasd_discipline {
* Extent Space Efficient (ESE) relevant functions
*/
int (*is_ese)(struct dasd_device *);
+ int (*ese_capable)(struct dasd_device *);
+ /* Whether the volume is formatted on demand (thin), from the label */
+ int (*on_demand_format)(struct dasd_device *);
/* Capacity */
int (*space_allocated)(struct dasd_device *);
int (*space_configured)(struct dasd_device *);
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (14 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:45 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes Stefan Haberland
2026-08-01 11:10 ` [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path Stefan Haberland
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Extend the device information line logged when a volume comes online with
the ESE hardware capability and the on-disk format mode. The format mode
(full or on demand) is derived from the on-disk format label alone, so a
volume that is not backed by ESE hardware but was still formatted on
demand is reported correctly.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 69a6a462e3e2..428a691ed5a1 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -2522,6 +2522,7 @@ static int dasd_eckd_end_analysis(struct dasd_block *block)
struct dasd_device *device = block->base;
struct dasd_eckd_private *private = device->private;
struct eckd_count *count_area;
+ const char *ese_str, *fmt_str;
unsigned int sb, blk_per_trk;
int status, i;
struct dasd_ccw_req *init_cqr;
@@ -2608,15 +2609,29 @@ static int dasd_eckd_end_analysis(struct dasd_block *block)
private->rdc_data.trk_per_cyl *
blk_per_trk);
+ /*
+ * Report the ESE hardware capability and the format mode. The mode
+ * comes from dasd_eckd_on_demand_format() (the on-disk label, or the
+ * ESE state when no label is present), matching the on_demand_formatting
+ * sysfs attribute.
+ */
+ ese_str = dasd_eckd_ese_capable(device) ? ", ESE" : "";
+ fmt_str = "";
+ if (dasd_eckd_on_demand_format(device))
+ fmt_str = ", on-demand format";
+ else if (dasd_eckd_ese_capable(device))
+ fmt_str = ", full format";
+
dev_info(&device->cdev->dev,
- "DASD with %u KB/block, %lu KB total size, %u KB/track, "
- "%s\n", (block->bp_block >> 10),
+ "DASD with %u KB/block, %lu KB total size, %u KB/track, %s%s%s\n",
+ (block->bp_block >> 10),
(((unsigned long) private->real_cyl *
private->rdc_data.trk_per_cyl *
blk_per_trk * (block->bp_block >> 9)) >> 1),
((blk_per_trk * block->bp_block) >> 10),
private->uses_cdl ?
- "compatible disk layout" : "linux disk layout");
+ "compatible disk layout" : "linux disk layout",
+ ese_str, fmt_str);
return 0;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (15 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:45 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path Stefan Haberland
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
Re-enable block-layer discard for ESE ECKD volumes, releasing thin space
via release allocated space (RAS).
This is based on
commit 7e64db1597fe ("s390/dasd: Add discard support for ESE volumes")
but adapted to the current code and fixed.
REQ_OP_DISCARD is routed to a RAS release over the request's track range,
and discard requests run on the base device only. Discard limits use extent
granularity via the disc_limits discipline hook so the block layer only
issues extent-aligned discards.
Discard is gated on the DASD_FEATURE_DISCARD device feature rather than a
per-discipline flag: the driver sets the feature when the volume is on ESE
hardware (i.e. RAS is available), and the block-layer setup enables discard
limits for a device that has it.
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd.c | 35 +++++--
drivers/s390/block/dasd_eckd.c | 173 ++++++++++++++++++++++++++-------
drivers/s390/block/dasd_int.h | 2 +
3 files changed, 167 insertions(+), 43 deletions(-)
diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
index 6a01d2140378..f6eb50e4174a 100644
--- a/drivers/s390/block/dasd.c
+++ b/drivers/s390/block/dasd.c
@@ -354,17 +354,19 @@ static int dasd_state_basic_to_ready(struct dasd_device *device)
*/
lim.dma_alignment = lim.logical_block_size - 1;
- if (device->discipline->has_discard) {
+ if (device->features & DASD_FEATURE_DISCARD) {
unsigned int max_bytes;
- lim.discard_granularity = block->bp_block;
-
- /* Calculate max_discard_sectors and make it PAGE aligned */
- max_bytes = USHRT_MAX * block->bp_block;
- max_bytes = ALIGN_DOWN(max_bytes, PAGE_SIZE);
-
- lim.max_hw_discard_sectors = max_bytes / block->bp_block;
- lim.max_write_zeroes_sectors = lim.max_hw_discard_sectors;
+ if (device->discipline->disc_limits) {
+ device->discipline->disc_limits(block, &lim);
+ } else {
+ lim.discard_granularity = block->bp_block;
+ /* Calculate max_discard_sectors and make it PAGE aligned */
+ max_bytes = USHRT_MAX * block->bp_block;
+ max_bytes = ALIGN_DOWN(max_bytes, PAGE_SIZE);
+ lim.max_hw_discard_sectors = max_bytes / block->bp_block;
+ lim.max_write_zeroes_sectors = lim.max_hw_discard_sectors;
+ }
}
rc = queue_limits_commit_update(block->gdp->queue, &lim);
if (rc)
@@ -3123,6 +3125,7 @@ static blk_status_t do_dasd_request(struct blk_mq_hw_ctx *hctx,
struct dasd_device *basedev;
struct dasd_ccw_req *cqr;
blk_status_t rc = BLK_STS_OK;
+ bool complete_noop = false;
basedev = block->base;
spin_lock_irq(&dq->lock);
@@ -3171,6 +3174,18 @@ static blk_status_t do_dasd_request(struct blk_mq_hw_ctx *hctx,
rc = BLK_STS_RESOURCE;
} else if (PTR_ERR(cqr) == -EINVAL) {
rc = BLK_STS_INVAL;
+ } else if (PTR_ERR(cqr) == -EOPNOTSUPP) {
+ /*
+ * A discard that covers no whole extent releases
+ * nothing. Discard is advisory, so complete it as a
+ * benign no-op: the device does support discard, this
+ * range just does not align to the large ESE extent
+ * granularity. Returning BLK_STS_NOTSUPP would make
+ * filesystems treat the device as lacking discard and
+ * stop issuing it. Completed after the lock is dropped.
+ */
+ rc = BLK_STS_OK;
+ complete_noop = true;
} else {
DBF_DEV_EVENT(DBF_ERR, basedev,
"CCW creation failed (rc=%ld) on request %p",
@@ -3204,6 +3219,8 @@ static blk_status_t do_dasd_request(struct blk_mq_hw_ctx *hctx,
out:
spin_unlock_irq(&dq->lock);
+ if (complete_noop)
+ blk_mq_end_request(req, BLK_STS_OK);
return rc;
}
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index 428a691ed5a1..e088dd216bbb 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -2326,6 +2326,18 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
/* Read Volume Information */
dasd_eckd_read_vol_info(device);
+ /*
+ * Advertise discard through the device feature so the block layer sets
+ * up discard limits. Discard releases allocated space, so require a thin
+ * (ESE) volume whose storage reports support for the space-release
+ * function. Raw-track access bypasses the normal block CCW path (discard
+ * would reach the raw builder, which has no record data), so exclude it.
+ */
+ if (dasd_eckd_ese_capable(device) &&
+ (private->features.feature[56] & 0x01) &&
+ !(device->features & DASD_FEATURE_USERAW))
+ device->features |= DASD_FEATURE_DISCARD;
+
/* Read the on-disk format label for ESE detection */
dasd_eckd_read_format_label(device);
@@ -4131,37 +4143,13 @@ static int dasd_eckd_ras_sanity_checks(struct dasd_device *device,
}
/*
- * Helper function to count the amount of involved extents within a given range
- * with extent alignment in mind.
+ * Number of extents the track range [from, to] spans. Extent n covers tracks
+ * [n * trks_per_ext, (n + 1) * trks_per_ext - 1], so the range touches the
+ * extents from (from / trks_per_ext) to (to / trks_per_ext) inclusive.
*/
static int count_exts(unsigned int from, unsigned int to, int trks_per_ext)
{
- int cur_pos = 0;
- int count = 0;
- int tmp;
-
- if (from == to)
- return 1;
-
- /* Count first partial extent */
- if (from % trks_per_ext != 0) {
- tmp = from + trks_per_ext - (from % trks_per_ext) - 1;
- if (tmp > to)
- tmp = to;
- cur_pos = tmp - from + 1;
- count++;
- }
- /* Count full extents */
- if (to - (from + cur_pos) + 1 >= trks_per_ext) {
- tmp = to - ((to - trks_per_ext + 1) % trks_per_ext);
- count += (tmp - (from + cur_pos) + 1) / trks_per_ext;
- cur_pos = tmp;
- }
- /* Count last partial extent */
- if (cur_pos < to)
- count++;
-
- return count;
+ return to / trks_per_ext - from / trks_per_ext + 1;
}
static int dasd_in_copy_relation(struct dasd_device *device)
@@ -4212,9 +4200,17 @@ dasd_eckd_dso_ras(struct dasd_device *device, struct dasd_block *block,
if (dasd_eckd_ras_sanity_checks(device, first_trk, last_trk))
return ERR_PTR(-EINVAL);
- copy_relation = dasd_in_copy_relation(device);
- if (copy_relation < 0)
- return ERR_PTR(copy_relation);
+ /*
+ * The block-layer discard path (req != NULL) runs in atomic context, so
+ * it must not issue the sleeping copy-relation (PPRC) query. It also
+ * leaves guarantee_init off - discard does not promise zeroing anyway.
+ */
+ copy_relation = 0;
+ if (!req) {
+ copy_relation = dasd_in_copy_relation(device);
+ if (copy_relation < 0)
+ return ERR_PTR(copy_relation);
+ }
rq = req ? blk_mq_rq_to_pdu(req) : NULL;
@@ -4246,7 +4242,7 @@ dasd_eckd_dso_ras(struct dasd_device *device, struct dasd_block *block,
* not fully specified, but is only supported with a certain feature
* subset and for devices not in a copy relation.
*/
- if (features->feature[56] & 0x01 && !copy_relation)
+ if (!req && features->feature[56] & 0x01 && !copy_relation)
ras_data->op_flags.guarantee_init = 1;
ras_data->lss = private->conf.ned->ID;
@@ -4342,6 +4338,9 @@ static int dasd_eckd_release_space_trks(struct dasd_device *device,
INIT_LIST_HEAD(&ras_queue);
+ if (dasd_eckd_ext_size(device) == 0)
+ return -EINVAL;
+
device_exts = private->real_cyl / dasd_eckd_ext_size(device);
trks_per_ext = dasd_eckd_ext_size(device) * private->rdc_data.trk_per_cyl;
@@ -5479,6 +5478,58 @@ dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *startdev,
return ERR_PTR(ret);
}
+static struct dasd_ccw_req *
+dasd_eckd_build_cp_discard(struct dasd_device *device, struct dasd_block *block,
+ struct request *req, sector_t first_trk,
+ sector_t last_trk, unsigned int first_offs,
+ unsigned int last_offs, unsigned int blk_per_trk)
+{
+ struct dasd_eckd_private *private = device->private;
+ sector_t first_ext_trk, last_ext_end, last_ext_trk;
+ unsigned int trks_per_ext;
+
+ trks_per_ext = dasd_eckd_ext_size(device) * private->rdc_data.trk_per_cyl;
+ if (!trks_per_ext)
+ return ERR_PTR(-EOPNOTSUPP);
+
+ /*
+ * A discard range is rarely track-aligned: fstrim is FS-block granular
+ * and discard_granularity is only a hint. If it starts or ends mid-track,
+ * that boundary track still holds live records outside the range, so drop
+ * it from the whole-track span first. Otherwise a partial boundary track
+ * that happens to sit on an extent boundary would be released together
+ * with its live records resulting in silent data loss
+ */
+ if (first_offs) /* partial first track */
+ first_trk++;
+ if (last_offs != blk_per_trk - 1) { /* partial last track */
+ if (!last_trk)
+ return ERR_PTR(-EOPNOTSUPP);
+ last_trk--;
+ }
+ if (first_trk > last_trk)
+ return ERR_PTR(-EOPNOTSUPP); /* no whole track fully covered */
+
+ /*
+ * RAS releases whole extents. Only release extents that lie entirely
+ * within the (now whole-track) discard range by rounding inward to extent
+ * boundaries - an extent shared with a live allocation must never be
+ * released. If no whole extent is covered there is nothing to release
+ * safely (e.g. a sub-extent discard, unavoidable with large extents), so
+ * reject the request rather than release too much.
+ */
+ first_ext_trk = roundup(first_trk, trks_per_ext);
+ /* one past the last whole extent inside the range (exclusive) */
+ last_ext_end = rounddown(last_trk + 1, trks_per_ext);
+ if (first_ext_trk >= last_ext_end)
+ return ERR_PTR(-EOPNOTSUPP);
+ /* inclusive last track; the guard above keeps this from underflowing */
+ last_ext_trk = last_ext_end - 1;
+
+ return dasd_eckd_dso_ras(device, block, req, first_ext_trk,
+ last_ext_trk, 1);
+}
+
static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
struct dasd_block *block,
struct request *req)
@@ -5517,6 +5568,12 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
last_offs = sector_div(last_trk, blk_per_trk);
cdlspecial = (private->uses_cdl && first_rec < 2*blk_per_trk);
+ if (req_op(req) == REQ_OP_DISCARD)
+ return dasd_eckd_build_cp_discard(startdev, block, req,
+ first_trk, last_trk,
+ first_offs, last_offs,
+ blk_per_trk);
+
fcx_multitrack = private->features.feature[40] & 0x20;
data_size = blk_rq_bytes(req);
if (data_size % blksize || data_size == 0)
@@ -5829,11 +5886,13 @@ static struct dasd_ccw_req *dasd_eckd_build_alias_cp(struct dasd_device *base,
struct request *req)
{
struct dasd_eckd_private *private;
- struct dasd_device *startdev;
+ struct dasd_device *startdev = NULL;
unsigned long flags;
struct dasd_ccw_req *cqr;
- startdev = dasd_alias_get_start_dev(base);
+ /* Discard requests (space release) can only run on the base device. */
+ if (req_op(req) != REQ_OP_DISCARD)
+ startdev = dasd_alias_get_start_dev(base);
if (!startdev)
startdev = base;
private = startdev->private;
@@ -7721,6 +7780,51 @@ static unsigned int dasd_eckd_max_sectors(struct dasd_block *block)
return DASD_ECKD_MAX_BLOCKS << block->s2b_shift;
}
+/*
+ * Discard on ECKD releases space through RAS, which works on whole extents.
+ * Advertise extent granularity so the block layer only sends extent-aligned
+ * discards (avoiding partially specified extents), and only for volumes on ESE
+ * hardware. Non-ESE devices are left without discard limits.
+ */
+static void dasd_eckd_disc_limits(struct dasd_block *block,
+ struct queue_limits *lim)
+{
+ struct dasd_device *device = block->base;
+ struct dasd_eckd_private *private = device->private;
+ unsigned int logical_block_size = block->bp_block;
+ unsigned int max_discard_sectors, max_bytes, ext_bytes;
+ int recs_per_trk, trks_per_cyl, ext_limit, ext_size;
+
+ if (!dasd_eckd_ese_capable(device) || dasd_eckd_ext_size(device) == 0)
+ return;
+
+ trks_per_cyl = private->rdc_data.trk_per_cyl;
+ recs_per_trk = recs_per_track(&private->rdc_data, 0, logical_block_size);
+
+ ext_size = dasd_eckd_ext_size(device);
+ ext_limit = min(private->real_cyl / ext_size, DASD_ECKD_RAS_EXTS_MAX);
+ ext_bytes = ext_size * trks_per_cyl * recs_per_trk * logical_block_size;
+ if (!ext_bytes) /* malformed RDC data - leave discard unset */
+ return;
+ max_bytes = UINT_MAX - (UINT_MAX % ext_bytes);
+ if (max_bytes / ext_bytes > ext_limit)
+ max_bytes = ext_bytes * ext_limit;
+
+ max_discard_sectors = max_bytes / 512;
+
+ lim->max_hw_discard_sectors = max_discard_sectors;
+ /*
+ * ext_bytes is the hardware extent size and is not a power of two, so
+ * the block layer's power-of-two round_up()/round_down() alignment
+ * helpers compute it only approximately. That is a hint, not a
+ * correctness requirement: RAS safety is enforced in the CCW builder,
+ * which rounds the range inward to whole extents and rejects a request
+ * that covers no whole extent, so a misaligned range is never
+ * over-released. At worst a few sub-extent discards are declined.
+ */
+ lim->discard_granularity = ext_bytes;
+}
+
static struct ccw_driver dasd_eckd_driver = {
.driver = {
.name = "dasd-eckd",
@@ -7743,6 +7847,7 @@ static struct dasd_discipline dasd_eckd_discipline = {
.owner = THIS_MODULE,
.name = "ECKD",
.ebcname = "ECKD",
+ .disc_limits = dasd_eckd_disc_limits,
.check_device = dasd_eckd_check_characteristics,
.uncheck_device = dasd_eckd_uncheck_device,
.do_analysis = dasd_eckd_do_analysis,
diff --git a/drivers/s390/block/dasd_int.h b/drivers/s390/block/dasd_int.h
index f61af45deab0..26ecf9f47d2e 100644
--- a/drivers/s390/block/dasd_int.h
+++ b/drivers/s390/block/dasd_int.h
@@ -404,6 +404,8 @@ struct dasd_discipline {
int (*ese_capable)(struct dasd_device *);
/* Whether the volume is formatted on demand (thin), from the label */
int (*on_demand_format)(struct dasd_device *);
+ /* Fill discard queue limits */
+ void (*disc_limits)(struct dasd_block *, struct queue_limits *);
/* Capacity */
int (*space_allocated)(struct dasd_device *);
int (*space_configured)(struct dasd_device *);
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
` (16 preceding siblings ...)
2026-08-01 11:10 ` [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes Stefan Haberland
@ 2026-08-01 11:10 ` Stefan Haberland
2026-08-01 11:49 ` sashiko-bot
17 siblings, 1 reply; 37+ messages in thread
From: Stefan Haberland @ 2026-08-01 11:10 UTC (permalink / raw)
To: linux-s390; +Cc: Jan Hoeppner, Eduard Shishkin
The CCW build path (prefix_LRE, the full-track prefix and dso_ras) read the
base address and LSS straight from conf.ned. That buffer is freed and
reallocated by the reload worker (do_reload_device - dasd_eckd_read_conf -
dasd_eckd_clear_conf_data), so a configuration change concurrent with I/O
can free conf.ned while a request is being built.
Use-after-free reported by KASAN in prefix_LRE.
Read the cached copies instead.
The unit address is already kept in uid.real_unit_addr, and the LSS is now
cached in ned_lss. Both are refreshed under the ccwdev lock in
dasd_eckd_generate_uid whenever the configuration is (re)read.
Also fix for prepare for read subsystem data (prssd) users.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
---
drivers/s390/block/dasd_eckd.c | 26 ++++++++++++++++----------
drivers/s390/block/dasd_eckd.h | 8 ++++++++
2 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
index e088dd216bbb..d5be9aaf41ed 100644
--- a/drivers/s390/block/dasd_eckd.c
+++ b/drivers/s390/block/dasd_eckd.c
@@ -588,8 +588,9 @@ static int prefix_LRE(struct ccw1 *ccw, struct PFX_eckd_data *pfxdata,
return -EINVAL;
}
pfxdata->format = format;
- pfxdata->base_address = basepriv->conf.ned->unit_addr;
- pfxdata->base_lss = basepriv->conf.ned->ID;
+ /* cached copies - conf.ned may be freed under us by the reload worker */
+ pfxdata->base_address = basepriv->ned_ua;
+ pfxdata->base_lss = basepriv->ned_lss;
pfxdata->validity.define_extent = 1;
/* private uid is kept up to date, conf_data may be outdated */
@@ -806,6 +807,9 @@ static int dasd_eckd_generate_uid(struct dasd_device *device)
return -ENODEV;
spin_lock_irqsave(get_ccwdev_lock(device->cdev), flags);
create_uid(&private->conf, &private->uid);
+ /* cache LSS and unit address for the lockless CCW-build path */
+ private->ned_lss = private->conf.ned->ID;
+ private->ned_ua = private->conf.ned->unit_addr;
spin_unlock_irqrestore(get_ccwdev_lock(device->cdev), flags);
return 0;
}
@@ -1631,8 +1635,8 @@ static int dasd_eckd_read_vol_info(struct dasd_device *device)
prssdp = cqr->data;
prssdp->order = PSF_ORDER_PRSSD;
prssdp->suborder = PSF_SUBORDER_VSQ; /* Volume Storage Query */
- prssdp->lss = private->conf.ned->ID;
- prssdp->volume = private->conf.ned->unit_addr;
+ prssdp->lss = private->ned_lss;
+ prssdp->volume = private->ned_ua;
ccw = cqr->cpaddr;
ccw->cmd_code = DASD_ECKD_CCW_PSF;
@@ -4245,8 +4249,9 @@ dasd_eckd_dso_ras(struct dasd_device *device, struct dasd_block *block,
if (!req && features->feature[56] & 0x01 && !copy_relation)
ras_data->op_flags.guarantee_init = 1;
- ras_data->lss = private->conf.ned->ID;
- ras_data->dev_addr = private->conf.ned->unit_addr;
+ /* cached copies - conf.ned may be freed under us by the reload worker */
+ ras_data->lss = private->ned_lss;
+ ras_data->dev_addr = private->ned_ua;
ras_data->nr_exts = nr_exts;
if (by_extent) {
@@ -4817,8 +4822,9 @@ static int prepare_itcw(struct itcw *itcw,
lredata = &pfxdata->locate_record;
pfxdata->format = 1; /* PFX with LRE */
- pfxdata->base_address = basepriv->conf.ned->unit_addr;
- pfxdata->base_lss = basepriv->conf.ned->ID;
+ /* cached copies - conf.ned may be freed under us by the reload worker */
+ pfxdata->base_address = basepriv->ned_ua;
+ pfxdata->base_lss = basepriv->ned_lss;
pfxdata->validity.define_extent = 1;
/* private uid is kept up to date, conf_data may be outdated */
@@ -6899,8 +6905,8 @@ static int dasd_eckd_query_host_access(struct dasd_device *device,
prssdp->order = PSF_ORDER_PRSSD;
prssdp->suborder = PSF_SUBORDER_QHA; /* query host access */
/* LSS and Volume that will be queried */
- prssdp->lss = private->conf.ned->ID;
- prssdp->volume = private->conf.ned->unit_addr;
+ prssdp->lss = private->ned_lss;
+ prssdp->volume = private->ned_ua;
/* all other bytes of prssdp must be zero */
ccw = cqr->cpaddr;
diff --git a/drivers/s390/block/dasd_eckd.h b/drivers/s390/block/dasd_eckd.h
index 30745f62402b..8e6f09e9ca7e 100644
--- a/drivers/s390/block/dasd_eckd.h
+++ b/drivers/s390/block/dasd_eckd.h
@@ -736,6 +736,14 @@ struct dasd_eckd_private {
/* alias management */
struct dasd_uid uid;
+ /*
+ * Cached copies of conf.ned->ID (the LSS) and conf.ned->unit_addr,
+ * refreshed under ccwdev_lock. Kept outside uid because create_uid()
+ * memsets uid before repopulating it, which would expose a transient
+ * zero to the lockless CCW-build readers.
+ */
+ __u8 ned_lss;
+ __u8 ned_ua;
struct alias_pav_group *pavgroup;
struct alias_lcu *lcu;
int count;
--
2.53.0
^ permalink raw reply related [flat|nested] 37+ messages in thread
* Re: [PATCH v6 06/18] s390/dasd: Add defines for the Extended Address Volume track address
2026-08-01 11:09 ` [PATCH v6 06/18] s390/dasd: Add defines for the Extended Address Volume track address Stefan Haberland
@ 2026-08-01 11:13 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:13 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, Heiko Carstens, linux-s390, Alexander Gordeev
> The track address of an Extended Address Volume (more than 65520
> cylinders) carries the high cylinder bits that do not fit the 16-bit cyl
> field in the upper part of the head field. set_ch_t() open-codes the
> corresponding shifts; name them so the encoding is explicit and can be
> reused.
>
> No functional change.
>
> Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
> Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=6
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 04/18] s390/dasd: Optimize max blocks per request for track alignment
2026-08-01 11:09 ` [PATCH v6 04/18] s390/dasd: Optimize max blocks per request for track alignment Stefan Haberland
@ 2026-08-01 11:20 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:20 UTC (permalink / raw)
To: Stefan Haberland
Cc: Alexander Gordeev, Vasily Gorbik, linux-s390, Heiko Carstens
> With 4096-byte blocks a full ECKD track holds exactly 12 records. Lower
> DASD_ECKD_MAX_BLOCKS from 190 to 180 so requests align to track
> boundaries (15 full tracks); full-track I/O is more efficient than
> partial-track writes, and 190 had no alignment significance and could
> let a request cross a track boundary.
>
> Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
> Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=4
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 02/18] s390/dasd: Propagate partial completion length across ERP recovery
2026-08-01 11:09 ` [PATCH v6 02/18] s390/dasd: Propagate partial completion length across ERP recovery Stefan Haberland
@ 2026-08-01 11:21 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:21 UTC (permalink / raw)
To: Stefan Haberland
Cc: Alexander Gordeev, linux-s390, Heiko Carstens, Vasily Gorbik
> dasd_default_erp_postaction() copies the timing and device state from
> the finished ERP request back to the original request but drops
> proc_bytes. A request that was partially completed, an ESE read of a
> not-yet-allocated track returns fewer bytes than requested, and then
> recovered through the ERP chain loses its partial-completion length.
> __dasd_cleanup_cqr() then sees proc_bytes == 0 and completes the whole
> request instead of requeueing the remainder, silently returning zeroed
> data for the part that was never read.
>
> Carry proc_bytes over to the original request like the other
> per-request state.
>
> Fixes: 5e6bdd37c552 ("s390/dasd: fix data corruption for thin provisioned devices")
> Cc: stable@vger.kernel.org
> Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
> Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=2
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 05/18] s390/dasd: Use GFP_KERNEL in dasd_alloc_device()
2026-08-01 11:09 ` [PATCH v6 05/18] s390/dasd: Use GFP_KERNEL in dasd_alloc_device() Stefan Haberland
@ 2026-08-01 11:21 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:21 UTC (permalink / raw)
To: Stefan Haberland
Cc: linux-s390, Alexander Gordeev, Heiko Carstens, Vasily Gorbik
> dasd_alloc_device() runs in process context (device set_online), so its
> pool allocations do not need GFP_ATOMIC. Use GFP_KERNEL instead, which is
> more reliable, especially for the larger DMA allocations that later ESE
> full-track work adds here.
>
> No functional change intended.
>
> Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
> Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=5
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode
2026-08-01 11:10 ` [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode Stefan Haberland
@ 2026-08-01 11:27 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:27 UTC (permalink / raw)
To: Stefan Haberland
Cc: linux-s390, Heiko Carstens, Vasily Gorbik, Alexander Gordeev
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The `full_track_bias` parameter is unconditionally applied to all ECKD devices, inflicting performance penalties on standard non-ESE volumes.
- [Medium] Inconsistent handling of `full_track_bias` values 1-99 between device initialization and the sysfs store function.
- [Medium] Race condition in sysfs store method allows a torn update to permanently desync the `ft_bias` and `fulltrack` state.
--
--- Patch [12]: [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode ---
commit cca10efd2af8b0fe1b751764fcdbf7d7f54c4ef1
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Add full_track_bias to control fulltrack write mode
Add a per-device full_track_bias sysfs attribute to gate the full-track
write path for ESE devices. A module parameter sets the initial default value
applied to devices at online time.
> diff --git a/drivers/s390/block/dasd_devmap.c b/drivers/s390/block/dasd_devmap.c
> index 381d616ad433f..035c022255b68 100644
> --- a/drivers/s390/block/dasd_devmap.c
> +++ b/drivers/s390/block/dasd_devmap.c
> @@ -1630,6 +1630,44 @@ dasd_expires_store(struct device *dev, struct device_attribute *attr,
>
[ ... ]
> +static ssize_t full_track_bias_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dasd_device *device;
> + unsigned int val;
> +
> + if (kstrtouint(buf, 0, &val) || val > DASD_FT_BIAS_MAX)
> + return -EINVAL;
> +
> + device = dasd_device_from_cdev(to_ccwdev(dev));
> + if (IS_ERR(device))
> + return -ENODEV;
> +
> + device->ft_bias = val;
> + device->fulltrack = val ? 1 : 0;
[Severity: Medium]
Is it possible for concurrent sysfs writes to leave these variables out of
sync?
Because the sysfs core does not serialize concurrent writes to the same file
if opened via separate file descriptors, could an interleaved execution from
two threads writing 100 and 0 concurrently result in a torn update where
ft_bias is 0 but fulltrack remains 1?
[Severity: Medium]
Does this sysfs store function contradict the initialization logic for values
between 1 and 99?
During device initialization, dasd_eckd_check_characteristics() restricts
device->fulltrack to 0 for a value like 50 (the default). However, if the
exact same value (50) is written to this sysfs attribute, this line
unconditionally sets device->fulltrack to 1.
Could this cause a sudden unprompted change in I/O behavior upon rewriting
the current configuration value?
> +
> + dasd_put_device(device);
> + return count;
> +}
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 5e7a531707a0c..9003e28a4ebbe 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
> @@ -2148,6 +2160,11 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
> device->path_interval = DASD_ECKD_PATH_INTERVAL;
> device->aq_timeouts = DASD_RETRIES_MAX;
>
> + /* default ESE fulltrack write aggressiveness from the module parameter */
> + device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
> + /* only the "always" endpoint forces fulltrack unconditionally here */
> + device->fulltrack = (device->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
[Severity: High]
Will this module parameter be applied to all ECKD devices, even standard
non-ESE volumes?
The commit message states the bias should be applied to every ESE volume at
online time. However, this appears to unconditionally copy the module
parameter into every ECKD device's state regardless of whether the volume
is actually ESE, without checking dasd_eckd_is_ese(device).
> @@ -5199,11 +5216,20 @@ static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
> /* do nothing, just fall through to the cmd mode single case */
> } else if ((data_size <= private->fcx_max_data)
> && (fcx_multitrack || (first_trk == last_trk))) {
> - cqr = dasd_eckd_build_cp_tpm_track(startdev, block, req,
> - first_rec, last_rec,
> - first_trk, last_trk,
> - first_offs, last_offs,
> - blk_per_trk, blksize);
> + if (!first_offs && (last_offs + 1 == blk_per_trk) &&
> + rq_data_dir(req) == WRITE && basedev->fulltrack) {
> + cqr = dasd_eckd_build_cp_tpm_writefulltrack(startdev, block, req,
> + first_rec, last_rec,
> + first_trk, last_trk,
> + first_offs, last_offs,
> + blk_per_trk, blksize, NULL);
[Severity: High]
If basedev->fulltrack is set for a non-ESE device, will this downgrade
standard track-based I/O to WRITE_FULL_TRACK?
If an administrator sets dasd_eckd_mod.full_track_bias=100 to optimize
their Extent Space Efficient (ESE) volumes, it looks like all standard
(non-ESE) volumes will indiscriminately inherit this setting and suffer a
severe maximum throughput degradation penalty.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=12
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful
2026-08-01 11:09 ` [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful Stefan Haberland
@ 2026-08-01 11:27 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:27 UTC (permalink / raw)
To: Stefan Haberland
Cc: Heiko Carstens, Vasily Gorbik, Alexander Gordeev, linux-s390
> dasd_int_handler() completes an NRF read of an unallocated ESE track by
> calling ese_read() and unconditionally marking the request
> DASD_CQR_SUCCESS. dasd_eckd_ese_read() can return an error before it has
> zeroed the destination buffer: a failed sense-data parse or a current
> track outside the requested range both return early, leaving the
> destination pages untouched. The request is still completed successfully,
> so the block layer is handed stale / uninitialized memory instead of
> zeros.
>
> Check the ese_read() return value and fail the request through the normal
> error path instead of forcing DASD_CQR_SUCCESS.
>
> Fixes: 5e6bdd37c552 ("s390/dasd: fix data corruption for thin provisioned devices")
> Cc: stable@vger.kernel.org
> Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=1
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack()
2026-08-01 11:10 ` [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack() Stefan Haberland
@ 2026-08-01 11:29 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:29 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, Alexander Gordeev, Heiko Carstens, linux-s390
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
New issues:
- [Critical] Missing bio_has_data() guard before iterating request segments.
- [High] Negative bitshift in prepare_itcw() exposed by unrestricted track count.
- [High] Premature loop termination and disk format corruption due to split bio_vec segments being mapped 1:1 to disk records.
- [Medium] Silent bypass of errors from prepare_itcw() other than -EAGAIN.
Pre-existing issues:
- [Critical] Pre-existing missing bio_has_data() guards in CMD and TPM track builders.
--
--- Patch [10]: [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack() ---
commit c49275335fe037682ab8442e739559068d7eb61c
Author: Stefan Haberland <sth@linux.ibm.com>
Subject: s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack()
This commit adds the channel program builder for WRITE_FULL_TRACK requests.
It builds an ITCW with a TIDAW list for formatting and writing tracks
atomically on ESE devices.
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
[ ... ]
> +static __maybe_unused struct dasd_ccw_req *
> +dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *startdev,
[ ... ]
> + trkcount = last_trk - first_trk + 1;
> + ctidaw = trkcount * 2 * (blk_per_trk + 2);
[ ... ]
> + if (prepare_itcw(itcw, first_trk, last_trk,
> + cmd, basedev, startdev,
> + 0,
> + trkcount, blksize,
> + datasize,
> + tlf,
> + blk_per_trk) == -EAGAIN) {
[Severity: Medium]
Does this code handle other negative error codes from prepare_itcw() properly?
If prepare_itcw() returns an error like -EINVAL, the check for -EAGAIN
evaluates to false, and the function will continue as if it succeeded. Could
this submit an incomplete ITCW program to the hardware?
[Severity: High]
Can this lead to a negative bitshift in prepare_itcw()?
The trkcount variable is calculated based on last_trk - first_trk + 1 and
passed as the count parameter to prepare_itcw(). Looking at prepare_itcw()
for the DASD_ECKD_CCW_WRITE_FULL_TRACK case:
prepare_itcw() {
...
case DASD_ECKD_CCW_WRITE_FULL_TRACK:
...
if (count > 8) {
lredata->extended_parameter[0] = 0xFF;
lredata->extended_parameter[1] = 0xFF;
lredata->extended_parameter[1] <<= (16 - count);
If a request spans more than 16 tracks, the count passed in will exceed 16,
resulting in a negative shift amount. Should there be bounds checking on
trkcount before passing it here?
[ ... ]
> + rec_count = 0;
> + rq_for_each_segment(bv, req, iter) {
[Severity: Critical]
Does this loop need to verify that the bio actually contains data?
According to block layer subsystem guidelines, iterating segments without
checking bio_has_data() can cause a NULL pointer dereference for operations
that have no data buffers (such as REQ_OP_WRITE_ZEROES). Even if op_is_write()
is true, data fields shouldn't be accessed without this guard.
[ ... ]
> + last_tidaw = add_track_record(itcw, &filldata, &fillsize,
> + cyl, head, rec, dst, part_len);
> + if (!last_tidaw)
> + goto out_error;
[ ... ]
> + rec++;
> + dst += part_len;
> + rec_count++;
> + if (rec_count >= (last_rec - first_rec + 1))
> + goto out_skip;
> + }
> + }
[Severity: High]
Could this terminate the loop prematurely and corrupt the disk format?
If a logical block crosses a page boundary, the block layer will split it into
multiple smaller bio_vec segments. Because this loop increments rec_count for
every segment iteration and emits a track record, split segments would be
written as multiple short records rather than a single full block.
Should rec_count be incremented based on the accumulated logical block size
instead of mapping one-to-one with the bio_vec segments?
[ ... ]
> static struct dasd_ccw_req *dasd_eckd_build_cp_cmd_track(
[ ... ]
> rq_for_each_segment(bv, req, iter) {
[Severity: Critical]
This is a pre-existing issue, but does this function also need a
bio_has_data() guard before iterating segments?
Similar to the newly added dasd_eckd_build_cp_tpm_writefulltrack(),
iterating over request segments here after only checking
rq_data_dir(req) == WRITE will cause a NULL pointer dereference on
operations like REQ_OP_DISCARD or REQ_OP_WRITE_ZEROES that do not carry
data buffers.
[ ... ]
> static struct dasd_ccw_req *dasd_eckd_build_cp_tpm_track(
[ ... ]
> if (rq_data_dir(req) == WRITE) {
> new_track = 1;
> recid = first_rec;
> rq_for_each_segment(bv, req, iter) {
[Severity: Critical]
This is also a pre-existing issue, but does the loop in
dasd_eckd_build_cp_tpm_track() suffer from the same missing bio_has_data()
guard?
Operations without data buffers will trigger a NULL pointer dereference
during segment iteration here as well.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=10
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write
2026-08-01 11:09 ` [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write Stefan Haberland
@ 2026-08-01 11:32 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:32 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, Alexander Gordeev, linux-s390, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] Logic Error / Double Completion. Aborted origin CQRs (DASD_CQR_ABORTED) are erroneously completed to the block layer, leading to double-completion and UAF of block requests, or infinite hangs for non-block requests.
Pre-existing issues:
- [High] Pre-existing Use-After-Free in `__dasd_cleanup_cqr()` when accessing `cqr->intrc` after the request memory has been freed.
--
--- Patch [7]: [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write ---
commit 6568e31c69d8658129394132fae86e53a21d2edd
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Add infrastructure for ESE full-track write
This commit adds the driver internals to build WRITE_FULL_TRACK FCX channel
programs for unformatted tracks on ESE devices. It introduces track-filler
buffers, hardirq-safe staging lists, and new CQR states to handle replaced
writes.
> diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
> --- a/drivers/s390/block/dasd.c
> +++ b/drivers/s390/block/dasd.c
> @@ -1223,7 +1250,18 @@ void dasd_sfree_request(struct dasd_ccw_req *cqr, struct dasd_device *device)
> unsigned long flags;
>
> spin_lock_irqsave(&device->mem_lock, flags);
> - dasd_free_chunk(&device->ccw_chunks, cqr->mem_chunk);
> + /*
> + * Free the request block from the pool it came from: smalloc() sets
> + * mem_chunk (ccw_chunks), fmalloc() leaves it NULL (ese_chunks). A
> + * full-track request also frees its track-filler buffer.
> + */
> + if (cqr->filldata)
> + dasd_free_chunk(&device->fill_chunks, cqr->filldata);
> + if (cqr->mem_chunk)
> + dasd_free_chunk(&device->ccw_chunks, cqr->mem_chunk);
> + else
> + dasd_free_chunk(&device->ese_chunks, cqr);
> +
> spin_unlock_irqrestore(&device->mem_lock, flags);
> dasd_put_device(device);
> }
[Severity: High]
This is a pre-existing issue, but does the block layer cleanup code access
the cqr memory after it is freed here?
Looking at the call chain starting from dasd_block_tasklet():
__dasd_cleanup_cqr() ->
discipline->free_cp() (e.g., dasd_eckd_free_cp()) ->
dasd_sfree_request()
After free_cp() returns, __dasd_cleanup_cqr() evaluates the request state:
else if (status == 0) {
switch (cqr->intrc) {
...
Since the cqr memory is returned to the chunk allocator pool inside
dasd_sfree_request(), could another CPU concurrently reallocate and
overwrite the cqr memory before cqr->intrc is read, resulting in a
use-after-free?
[ ... ]
> @@ -1884,6 +1924,10 @@ static void __dasd_process_cqr(struct dasd_device *device,
> case DASD_CQR_CLEARED:
> cqr->status = DASD_CQR_TERMINATED;
> break;
> + case DASD_CQR_ABORT:
> + cqr->status = DASD_CQR_ABORTED;
> + cqr->callback = NULL;
> + break;
> default:
> dev_err(&device->cdev->dev,
> "Unexpected CQR status %02x", cqr->status);
[ ... ]
> @@ -2761,7 +2806,8 @@ static void __dasd_process_block_ccw_queue(struct dasd_block *block,
> if (cqr->status != DASD_CQR_DONE &&
> cqr->status != DASD_CQR_FAILED &&
> cqr->status != DASD_CQR_NEED_ERP &&
> - cqr->status != DASD_CQR_TERMINATED)
> + cqr->status != DASD_CQR_TERMINATED &&
> + cqr->status != DASD_CQR_ABORTED)
> continue;
>
> if (cqr->status == DASD_CQR_TERMINATED) {
> @@ -2872,6 +2918,14 @@ static void dasd_block_tasklet(unsigned long data)
[Severity: High]
The commit message states that the DASD_CQR_ABORT state is used to "retire
the origin CQR of a replaced write without completing it to the block layer."
However, since __dasd_process_block_ccw_queue() accepts DASD_CQR_ABORTED
and moves it to the final_queue, will __dasd_cleanup_cqr() still process
it?
If __dasd_cleanup_cqr() processes the aborted CQR, it appears to
unconditionally call blk_mq_end_request(req, error) instead of skipping
it. When the replacement ESE format CQR later completes, won't it try to
complete the exact same struct request, leading to a double-completion in
the block layer?
Additionally, for non-block requests, does setting cqr->callback = NULL in
__dasd_process_cqr() skip dasd_wakeup_cb()? Could this cause a waiter to
hang indefinitely in wait_event() if the wakeup callback is never executed?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=7
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes
2026-08-01 11:10 ` [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes Stefan Haberland
@ 2026-08-01 11:33 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:33 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, Alexander Gordeev, linux-s390, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] Unsynchronized modification of the ese_format_quick state flag.
Pre-existing issues:
- [High] Heap buffer overflow in dasd_eckd_build_format when handling specialized format intensities.
--
--- Patch [14]: [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes ---
commit 1a0c863785cfe5455ad8a4386ef60684b5d42b93
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Stamp a format label into newly formatted volumes
When a CDL volume is formatted, write a small on-disk label so the format
can later be recognised by the kernel. The label records a magic, a version,
whether the volume is ESE, and whether it was formatted quick (space released,
thin) or full.
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 67c301e182c0f..ebffb3364c33c 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
[ ... ]
> @@ -2816,6 +2849,10 @@ dasd_eckd_build_format(struct dasd_device *base, struct dasd_device *startdev,
> return ERR_PTR(-EINVAL);
> }
>
> + /* room for the label data that R4 carries in addition to its count */
> + if (write_label)
> + datasize += fdata->blksize;
> +
[Severity: High]
This is a pre-existing issue, but can this code cause a heap buffer overflow?
When handling specialized format intensities (e.g., 0x01 or 0x09), the size
calculation in dasd_eckd_build_format() appears to under-calculate the
required allocation for multi-track requests by missing per-track R0
allocations:
dasd_eckd_build_format() {
...
case 0x01: /* Write record zero and format track. */
case 0x09: /* Write record zero and format track, use cdl. */
cplength = 2 + rpt * nr_tracks;
if (use_prefix)
datasize = sizeof(struct PFX_eckd_data) +
sizeof(struct LO_eckd_data) +
sizeof(struct eckd_count) +
rpt * nr_tracks * sizeof(struct eckd_count);
...
}
However, the subsequent loop unconditionally writes nr_tracks sets of CCWs and
eckd_count structures:
dasd_eckd_build_format() {
...
for (j = 0; j < nr_tracks; j++) {
...
if (intensity & 0x01) { /* write record zero */
ect = (struct eckd_count *) data;
data += sizeof(struct eckd_count);
...
ccw++;
...
}
Could this write past the allocated boundaries if a user with CAP_SYS_ADMIN
invokes the BIODASDFMT ioctl with a track range spanning multiple tracks?
> fcp = dasd_fmalloc_request(DASD_ECKD_MAGIC, cplength, datasize, startdev);
> if (IS_ERR(fcp))
> return fcp;
[ ... ]
> @@ -3177,8 +3231,18 @@ static int dasd_eckd_format_device(struct dasd_device *base,
> if (fdata->start_unit == 0)
> dasd_ft_bias_apply(base);
>
> - return dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
> - 0, NULL);
> + rc = dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
> + 0, NULL);
> +
> + /*
> + * The quick-format indicator was consumed by the label stamped into
> + * track 0; clear it so a later format that is not preceded by a full
> + * space release is recorded as a full format.
> + */
> + if (fdata->start_unit == 0)
> + private->ese_format_quick = 0;
[Severity: Medium]
Is it possible for this flag modification to race with concurrent updates?
If a concurrent BIODASDRAS ioctl is executing, dasd_eckd_release_space_full()
might update this state flag without locks:
dasd_eckd_release_space_full() {
...
private = device->private;
private->ese_format_quick = 1;
...
}
Because dasd_eckd_format_device() clears the flag unconditionally after an
unprotected blocking operation, could it overwrite the concurrent update?
This might cause the next disk format label to be incorrectly stamped as a
'full' format rather than a 'quick' format.
> +
> + return rc;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=14
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection
2026-08-01 11:09 ` [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection Stefan Haberland
@ 2026-08-01 11:36 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:36 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, Alexander Gordeev, linux-s390, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] Premature removal of the `trkcount` check introduces a double-format vulnerability that breaks bisection and causes data corruption.
- [Low] Unsynchronized modification of `format->cqr` outside of `format_lock` causes a data race.
--
--- Patch [8]: [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection ---
Note: The format of this review has been altered to a free-form summary to
comply with system recitation restrictions. Standard diff quoting has been
omitted.
commit a04e5d02e092ddaf9f460306f01f82bfd4b72118
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Add range-based format-track collision detection
This commit replaces the single format_entry with an array to support multiple
in-flight format requests, and modifies conflict detection to use range
overlaps. It also removes the trkcount snapshot in dasd_start_IO().
[Severity: Critical]
In dasd_start_IO(), the snapshot of trkcount was removed, which previously
protected against delayed-interrupt races. In test_and_set_format_track(),
this was replaced with a scan of format_list.
However, because the new collision flag mechanism is dormant in this patch,
does this premature removal introduce a double-format vulnerability that breaks
bisection?
If CPU 1 and CPU 2 both issue a WRITE to the same unallocated track, the
device fails both, generating two unformatted track interrupts. If CPU 1
processes its interrupt, formats the track, and retries its WRITE, it will
remove the track from format_list. If CPU 2 is delayed and processes its
interrupt after CPU 1s format completes, it calls test_and_set_format_track().
Since the track is no longer in format_list and the new collision mechanism is
inactive, CPU 2 will not detect the collision and will issue a second format,
potentially erasing CPU 1s freshly written data.
[Severity: Low]
In dasd_eckd_ese_format(), there is an assignment format->cqr = fcqr which
occurs outside of the format_lock.
Concurrently, in test_and_set_format_track(), a reader checks
device->format_entry[i].cqr while holding the lock.
Does this unsynchronized modification cause a data race on a shared pointer?
While it might not cause a crash since the pointer transitions from one valid
non-NULL pointer to another, it appears to violate kernel concurrency rules.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=8
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK
2026-08-01 11:09 ` [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK Stefan Haberland
@ 2026-08-01 11:37 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:37 UTC (permalink / raw)
To: Stefan Haberland
Cc: linux-s390, Vasily Gorbik, Heiko Carstens, Alexander Gordeev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] The newly added DASD_ECKD_CCW_WRITE_FULL_TRACK case correctly initializes fields but they are unconditionally overwritten by trailing common code in prepare_itcw().
- [High] Uninitialized `start_trk` and `end_trk` in non-format requests cause false-positive collision detection when track 0 is formatted.
--
--- Patch [9]: [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK ---
commit c073fedc91e7807d5ec588cea865d8b63661d671
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK
This commit extends prepare_itcw() to build the FCX prefix block (PFX +
LRE) for the new DASD_ECKD_CCW_WRITE_FULL_TRACK track-mode I/O command. It
allocates the prefix buffer on the stack to prevent memory allocation
failures under pressure during the writeback path.
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 77ceba151560b..01c6cdc8430fe 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
> @@ -4383,11 +4383,13 @@ static int prepare_itcw(struct itcw *itcw,
[ ... ]
> switch (cmd) {
> @@ -4446,7 +4453,38 @@ static int prepare_itcw(struct itcw *itcw,
> * data as well.
> */
> if (dedata->ga_extended & 0x08 && dedata->ga_extended & 0x02)
> - pfxdata.validity.time_stamp = 1; /* 'Time Stamp Valid' */
> + pfxdata->validity.time_stamp = 1; /* 'Time Stamp Valid' */
> + pfx_cmd = DASD_ECKD_CCW_PFX;
> + break;
> + case DASD_ECKD_CCW_WRITE_FULL_TRACK:
> + dedata->mask.perm = 0x3;
> + dedata->mask.auth = 0x00;
> + dedata->attributes.operation = basepriv->attrib.operation;
> + dedata->blk_size = blksize;
> + dedata->ga_extended |= 0x42;
> + rc = set_timestamp(NULL, dedata, basedev);
> + lredata->operation.orientation = 0x0;
> + lredata->operation.operation = 0x3F;
> + lredata->extended_operation = 0x11;
> + lredata->auxiliary.check_bytes = 0x2;
> + lredata->extended_parameter_length = 0x02;
> + if (count > 8) {
> + lredata->extended_parameter[0] = 0xFF;
> + lredata->extended_parameter[1] = 0xFF;
> + lredata->extended_parameter[1] <<= (16 - count);
> + } else {
> + lredata->extended_parameter[0] = 0xFF;
> + lredata->extended_parameter[0] <<= (8 - count);
> + lredata->extended_parameter[1] = 0x00;
> + }
> + sector = 0xFF;
> + /*
> + * If XRC is supported the System Time Stamp is set. The
> + * validity of the time stamp must be reflected in the prefix
> + * data as well.
> + */
> + if (dedata->ga_extended & 0x08 && dedata->ga_extended & 0x02)
> + pfxdata->validity.time_stamp = 1; /* 'Time Stamp Valid' */
> pfx_cmd = DASD_ECKD_CCW_PFX;
> break;
> case DASD_ECKD_CCW_READ_COUNT_MT:
[Severity: High]
Does the trailing code in prepare_itcw() overwrite these initialized fields
for the new full-track write command?
In the trailing code further down in prepare_itcw():
if (cmd == DASD_ECKD_CCW_READ_COUNT_MT) {
lredata->auxiliary.length_valid = 0;
lredata->auxiliary.length_scope = 0;
lredata->sector = 0xff;
} else {
lredata->auxiliary.length_valid = 1;
lredata->auxiliary.length_scope = 1;
lredata->sector = sector;
}
If rec_on_trk is non-zero, it will recalculate sector and overwrite the 0xFF
initialized above, and it will unconditionally set length_valid = 1 which is
incorrect for WRITE_FULL_TRACK. Should the exception condition be updated to
also exclude the new full-track write command?
[ ... ]
Additional note regarding track-based collision detection introduced in this
series:
[Severity: High]
Can uninitialized start_trk and end_trk in non-format requests cause
false-positive collision detection when track 0 is formatted?
Standard read/write requests are allocated via dasd_smalloc_request(), which
zeroes the request struct, but start_trk and end_trk are never initialized for
normal I/O requests (leaving them at 0).
When a format request completes on track 0 (format->start_trk == 0),
dasd_return_cqr_cb() evaluates dasd_req_conflict() against all in-flight
requests:
drivers/s390/block/dasd_int.h:dasd_req_conflict() {
...
return !(cqr1->format->end_trk < cqr2->start_trk ||
cqr2->end_trk < cqr1->format->start_trk);
}
For uninitialized standard I/O requests, the overlap condition
!(0 < 0 || 0 < 0) evaluates to true, erroneously flagging all in-flight
standard I/O requests with collision = true.
If those requests later receive an NRF error and require formatting, their
collision flag incorrectly tells test_and_set_format_track() they collided,
causing an immediate I/O retry without actually formatting the track. Does
this result in infinite retry loops and I/O failures on track 0?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=9
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label
2026-08-01 11:10 ` [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label Stefan Haberland
@ 2026-08-01 11:38 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:38 UTC (permalink / raw)
To: Stefan Haberland
Cc: Heiko Carstens, Alexander Gordeev, Vasily Gorbik, linux-s390
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] The in-memory ESE label cache (`private->ese_label_valid`) is not invalidated when track 0 is formatted without an ESE label, leaving the driver with a stale ESE state that no longer matches the physical disk.
Pre-existing issues:
- [High] Fatal NULL pointer dereference in `dasd_release_space()` for devices whose discipline lacks the `is_ese` method (e.g., FBA or DIAG devices).
--
--- Patch [15]: [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label ---
commit d17ebb7bd94d36c62afdabb020d82a8a18a64bba
Author: Stefan Haberland <sth@linux.ibm.com>
Subject: s390/dasd: Detect ESE volumes from the on-disk format label
This commit updates the DASD driver to detect Extent Space Efficient (ESE)
volumes by reading the format label from track 0 during device bring-up. It
caches this label and derives the ESE status from it, allowing volumes copied
from ESE storage to maintain their thin-provisioned handling on different
hardware.
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index ebffb3364c33c..35a970e006fd0 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
[ ... ]
> @@ -3224,23 +3353,35 @@ static int dasd_eckd_format_device(struct dasd_device *base,
> struct dasd_eckd_private *private = base->private;
> int rc;
>
> rc = dasd_eckd_format_process_data(base, fdata, enable_pav, 0, NULL,
> 0, NULL);
> + if (fdata->start_unit != 0)
> + return rc;
> +
> + if (rc) {
> + /*
> + * The format failed, so the label cached speculatively during
> + * CCW build may not match the disk; drop it so is_ese() falls
> + * back to the hardware field until the next successful format
> + * or bring-up.
> + */
> + private->ese_label_valid = false;
> + return rc;
> + }
[Severity: Medium]
When track 0 is formatted, what happens if no ESE label is written?
If a user issues the BIODASDFMT ioctl with start_unit == 0 and a non-CDL
layout (e.g., intensity & 0x08 == 0), dasd_eckd_build_format() might not
write a new label. The physical label on track 0 is then zeroed or destroyed
by the format.
However, dasd_eckd_format_device() only clears private->ese_label_valid on
failure. Could this regression leave the driver using a stale ESE state that
no longer matches the physical disk after a successful format?
[ ... ]
> @@ -4241,6 +4382,14 @@ static int dasd_eckd_release_space_trks(struct dasd_device *device,
> static int dasd_eckd_release_space(struct dasd_device *device,
> struct format_data_t *rdata)
> {
> + /*
> + * Space release (and thus a quick format) requires real ESE hardware.
> + * is_ese() may be true from a copied label on non-ESE hardware, so gate
> + * on the hardware capability, not on is_ese().
> + */
> + if (!dasd_eckd_ese_capable(device))
> + return -EOPNOTSUPP;
> +
[Severity: High]
This isn't a bug introduced by this patch, but does the ioctl path for space
release have a NULL pointer dereference for devices without an is_ese method?
In drivers/s390/block/dasd_ioctl.c, dasd_release_space() contains this check:
if (!device->discipline->is_ese && !device->discipline->is_ese(device))
return -ENOTSUPP;
Since it uses a logical AND (&&) instead of OR (||), if the discipline (e.g.,
FBA or DIAG) lacks the is_ese method, the first operand evaluates to true.
Will this force the evaluation of the second operand, immediately
dereferencing the NULL function pointer when a user issues the BIODASDRAS
ioctl?
> if (rdata->intensity & DASD_FMT_INT_ESE_FULL)
> return dasd_eckd_release_space_full(device);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=15
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler
2026-08-01 11:10 ` [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler Stefan Haberland
@ 2026-08-01 11:39 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:39 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, linux-s390, Alexander Gordeev, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [Critical] Skipping test_and_set_format_track() for full-track writes allows concurrent partial-track formats to overwrite valid data with zero-padding.
- [Medium] Double counting of profiling statistics for aborted ESE format base requests.
Pre-existing issues:
- [Critical] Lockless access to `cqr->collision` creates a Time-Of-Check to Time-Of-Use (TOCTOU) race that can lead to redundant track formatting and data corruption.
--
--- Patch [11]: [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler ---
commit d41c1ad780e71d8d577bfe81bd62b8114d2d1103
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Use WRITE_FULL_TRACK in ESE format handler
This commit wires dasd_eckd_build_cp_tpm_writefulltrack() into the ESE
unformatted track handler. It calculates the failing track ranges, builds a
WRITE_FULL_TRACK CQR, and aborts the origin CQR.
> diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
> index dfbadd83bea21..f1235729326ee 100644
> --- a/drivers/s390/block/dasd.c
> +++ b/drivers/s390/block/dasd.c
[ ... ]
> @@ -2754,6 +2758,9 @@ static void __dasd_cleanup_cqr(struct dasd_ccw_req *cqr)
> error = BLK_STS_IOERR;
> break;
> }
> + } else if (status == DASD_CQR_ABORTED) {
> + /* aborted requests are replaced with a new one so do not complete this */
> + return;
> }
>
> /*
[Severity: Medium]
Could this result in double-counting of profiling statistics?
dasd_profile_end() is called unconditionally at the beginning of
__dasd_cleanup_cqr(). Since the aborted CQR is replaced by a format request
that will also complete and go through this cleanup path, won't the same
block request be profiled twice in the block and device accumulators?
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 8e18dad96299f..5e7a531707a0c 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
[ ... ]
> @@ -3251,60 +3239,138 @@ dasd_eckd_ese_format(struct dasd_device *startdev, struct dasd_ccw_req *cqr,
[ ... ]
> + if (first_offs > 0 || last_offs + 1 < recs_per_trk) {
> + /* test if tracks are already in formatting by another thread */
> + if (test_and_set_format_track(first_trk, last_trk, cqr,
> + cqr->block, cqr->startdev, &format)) {
[Severity: Critical]
Is it safe to skip test_and_set_format_track() for full-track writes?
If a full-track write and a partial-track write are submitted concurrently
to the same unformatted track, the full-track write could skip acquiring the
format lock here. The partial-track write might then successfully acquire
the lock and build a zero-padded WRITE_FULL_TRACK CQR. If the partial-track
write executes on the device after the full-track write, couldn't it
overwrite the valid data from the full-track write with zeros?
[Severity: Critical]
This is a pre-existing issue, but is there a potential Time-Of-Check to
Time-Of-Use (TOCTOU) race inside test_and_set_format_track()?
Looking at test_and_set_format_track(), it checks cqr->collision without
holding a lock before acquiring block->format_lock:
test_and_set_format_track() {
if (cqr && cqr->collision) {
cqr->collision = false;
return true;
}
spin_lock_irqsave(&block->format_lock, flags);
...
}
If a partial-track write fails with INV_TRACK_FORMAT while another format
operation is in progress on the same track, and this lockless read sees a
stale false value, it proceeds to acquire the lock. If the format entry was
just cleared by the other CPU's tasklet, it might find an empty format list,
assume the track is unformatted, and issue a zero-padded WRITE_FULL_TRACK,
which would destroy the data just written by the previous format operation.
Could we read the collision flag inside the lock to enforce memory ordering?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=11
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data
2026-08-01 11:09 ` [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data Stefan Haberland
@ 2026-08-01 11:40 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:40 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, linux-s390, Alexander Gordeev, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
Pre-existing issues:
- [Critical] The patch fails to protect against a lockless Time-of-Check to Time-of-Use (TOCTOU) race where `device->private` is freed on the error path of `discipline->check_device()`, leading to a Use-After-Free or NULL pointer dereference.
- [High] The patch adds NULL checks for `device->private` in multiple sysfs-triggered callbacks but misses `dasd_eckd_pprc_enabled`, leaving a race condition that can panic the kernel.
- [High] The newly added `!private` checks are insufficient because callbacks subsequently dereference `private->conf.ned` before it is initialized.
- [High] In `dasd_release_space()`, an incorrect logical AND (`&&`) causes a NULL pointer dereference for disciplines without an `is_ese` callback, and silently bypasses the check for disciplines that have it.
- [High] The asynchronous attention interrupt handler path is vulnerable to the same `device->private` NULL pointer dereference race condition that the patch attempts to fix for sysfs.
--
--- Patch [3]: [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data ---
commit 698838157896167de2e3f1afbbb4fdad63518bec
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Guard sysfs discipline callbacks against unallocated private data
This patch adds NULL checks for device->private in several sysfs
show/store handlers inside dasd_eckd.c. It aims to fix a panic caused by
an unprivileged read of a sysfs attribute during the window between
discipline assignment and device->private allocation.
Cc: stable@vger.kernel.org
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 74fe73b5738ab..6e2fd445688cb 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
[ ... ]
> @@ -1646,6 +1648,9 @@ static int dasd_eckd_is_ese(struct dasd_device *device)
> {
> struct dasd_eckd_private *private = device->private;
>
> + if (!private)
> + return 0;
> +
> return private->vsq.vol_info.ese;
> }
[Severity: High]
This isn't a bug introduced by this patch, but does this pattern leave the
code vulnerable to a Time-of-Check to Time-of-Use race?
If a sysfs read calls dasd_eckd_is_ese() during dasd_generic_set_online(),
it can pass the new !private check. If the dasd_eckd_check_characteristics()
error path simultaneously frees device->private locklessly:
drivers/s390/block/dasd_eckd.c:dasd_eckd_check_characteristics() {
...
out_err1:
kfree(device->private);
device->private = NULL;
...
}
Could the sysfs handler then resume and dereference the freed memory when
accessing private->vsq.vol_info.ese?
[Severity: High]
This is a pre-existing issue, but while looking at the is_ese callback,
does dasd_release_space() in drivers/s390/block/dasd_ioctl.c have an issue
with its short-circuit logic?
drivers/s390/block/dasd_ioctl.c:dasd_release_space() {
...
if (!device->discipline->is_ese && !device->discipline->is_ese(device))
...
}
If the discipline does not implement is_ese (like FBA devices), wouldn't
the left operand evaluate to true, forcing evaluation of the right operand
and causing a NULL pointer dereference? And if it does implement is_ese,
wouldn't the left operand evaluate to false, bypassing the check entirely?
[ ... ]
> @@ -5935,8 +5965,11 @@ static int dasd_eckd_query_host_access(struct dasd_device *device,
> struct ccw1 *ccw;
> int rc;
>
> + if (!private)
> + return -ENODEV;
> +
> /* not available for HYPER PAV alias devices */
> - if (!device->block && private->lcu->pav == HYPER_PAV)
> + if (!device->block && private->lcu && private->lcu->pav == HYPER_PAV)
> return -EOPNOTSUPP;
[Severity: High]
This isn't a bug introduced by this patch, but is this check sufficient to
prevent dereferencing uninitialized pointers later in the function?
Inside dasd_eckd_check_characteristics(), device->private is allocated
immediately, but private->conf.ned is not populated until
dasd_eckd_read_conf() succeeds. If this sysfs handler executes during that
window, the !private check passes, but further down in
dasd_eckd_query_host_access() (and similarly in dasd_eckd_read_vol_info()),
the code performs:
prssdp->lss = private->conf.ned->ID;
prssdp->volume = private->conf.ned->unit_addr;
Since private->conf.ned would still be NULL during this window, can this
cause a NULL pointer dereference?
[ ... ]
> @@ -6801,6 +6834,9 @@ static int dasd_eckd_hpf_enabled(struct dasd_device *device)
> {
> struct dasd_eckd_private *private = device->private;
>
> + if (!private)
> + return 0;
> +
> return private->fcx_max_data ? 1 : 0;
> }
[Severity: High]
This is a pre-existing issue, but does dasd_eckd_pprc_enabled() also need
this NULL check?
If a user writes to the copy_pair sysfs attribute during device
initialization, dasd_copy_pair_store() locklessly calls the discipline
callback:
drivers/s390/block/dasd_devmap.c:dasd_copy_pair_store() {
...
pprc_enabled = device->discipline->pprc_enabled(device);
...
}
Inside dasd_eckd_pprc_enabled(), device->private is unconditionally
dereferenced:
drivers/s390/block/dasd_eckd.c:dasd_eckd_pprc_enabled() {
...
return private->rdc_data.facilities.PPRC_enabled;
}
Could this trigger a NULL pointer dereference if hit during the same
initialization window?
[Severity: High]
This is a pre-existing issue, but is the asynchronous attention interrupt
handler path vulnerable to this same race condition?
Since the subchannel is enabled during probe, a hardware attention interrupt
could arrive during the same device initialization window.
dasd_int_handler() locklessly calls device->discipline->check_attention(),
which schedules dasd_eckd_check_attention_work():
drivers/s390/block/dasd_eckd.c:dasd_eckd_check_attention() {
...
INIT_WORK(&data->worker, dasd_eckd_check_attention_work);
schedule_work(&data->worker);
...
}
This asynchronous work eventually calls dasd_eckd_cuir_quiesce() which
unconditionally dereferences private->lcu:
drivers/s390/block/dasd_eckd.c:dasd_eckd_cuir_quiesce() {
...
list_for_each_entry_safe(dev, n, &private->lcu->active_devices,
alias_list) {
...
}
If device->private is not yet allocated, does this cause a NULL pointer
dereference?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=3
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online
2026-08-01 11:10 ` [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online Stefan Haberland
@ 2026-08-01 11:45 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:45 UTC (permalink / raw)
To: Stefan Haberland
Cc: Alexander Gordeev, Vasily Gorbik, Heiko Carstens, linux-s390
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Lockless reading of partially updated `dasd_format_label` causes a data race and potential torn read in sysfs.
--
--- Patch [16]: [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online ---
commit 1bb0c0f3e976fb237f52d951feaeac8bfd58702e
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Report ESE capability and format mode at device online
This commit extends the device information line logged when a volume comes
online with the ESE hardware capability and the on-disk format mode. The
format mode is derived from the on-disk format label.
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 35a970e006fd0..643eb7882f9cd 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
> @@ -2608,15 +2609,29 @@ static int dasd_eckd_end_analysis(struct dasd_block *block)
> private->rdc_data.trk_per_cyl *
> blk_per_trk);
>
> + /*
> + * Report the ESE hardware capability and the format mode. The mode
> + * comes from dasd_eckd_on_demand_format() (the on-disk label, or the
> + * ESE state when no label is present), matching the on_demand_formatting
> + * sysfs attribute.
> + */
> + ese_str = dasd_eckd_ese_capable(device) ? ", ESE" : "";
> + fmt_str = "";
> + if (dasd_eckd_on_demand_format(device))
[Severity: Medium]
Could calling dasd_eckd_on_demand_format() locklessly result in a data race
and a potential torn read?
When reading from sysfs, or here in dasd_eckd_end_analysis(), we read the
label without locks:
drivers/s390/block/dasd_eckd.c:dasd_eckd_on_demand_format() {
...
if (private->ese_label_valid)
return !!(private->ese_label.flags & DASD_ESE_LABEL_F_QUICK);
...
}
However, other paths overwrite the label locklessly. This happens during
the format ioctl:
drivers/s390/block/dasd_eckd.c:dasd_eckd_fill_format_label() {
...
private->ese_label = *label;
private->ese_label_valid = true;
...
}
It also happens during device probe:
drivers/s390/block/dasd_eckd.c:dasd_eckd_read_format_label() {
...
private->ese_label = *label;
private->ese_label_valid = true;
...
}
If a user repeatedly reads the sysfs attribute (or this device online path
is executed) while concurrently running a format ioctl, could this cause a
torn read that returns a corrupted or uninitialized boolean value?
> + fmt_str = ", on-demand format";
> + else if (dasd_eckd_ese_capable(device))
> + fmt_str = ", full format";
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=16
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes
2026-08-01 11:10 ` [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes Stefan Haberland
@ 2026-08-01 11:45 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:45 UTC (permalink / raw)
To: Stefan Haberland
Cc: linux-s390, Heiko Carstens, Vasily Gorbik, Alexander Gordeev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Setting `discard_granularity` to a non-power-of-two value (`ext_bytes`) corrupts block layer alignment calculations, leading to undefined behavior and invalid sysfs limit checks.
- [Medium] Failing to clear discard limits when `DASD_FEATURE_DISCARD` is absent leaks stale state, silently dropping discards.
--
--- Patch [17]: [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes ---
commit a7ac6b0317116af2619de6aa7c00453f40467824
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Re-enable discard support for ESE volumes
Re-enable block-layer discard for ESE ECKD volumes, releasing thin space
via release allocated space (RAS). REQ_OP_DISCARD is routed to a RAS
release over the request's track range, and discard requests run on the
base device only. Discard limits use extent granularity via the disc_limits
discipline hook so the block layer only issues extent-aligned discards.
> diff --git a/drivers/s390/block/dasd.c b/drivers/s390/block/dasd.c
> --- a/drivers/s390/block/dasd.c
> +++ b/drivers/s390/block/dasd.c
> @@ -353,17 +353,19 @@ static int dasd_state_basic_to_ready(struct dasd_device *device)
> */
> lim.dma_alignment = lim.logical_block_size - 1;
>
> - if (device->discipline->has_discard) {
> + if (device->features & DASD_FEATURE_DISCARD) {
> unsigned int max_bytes;
>
> - lim.discard_granularity = block->bp_block;
> -
> - /* Calculate max_discard_sectors and make it PAGE aligned */
> - max_bytes = USHRT_MAX * block->bp_block;
> - max_bytes = ALIGN_DOWN(max_bytes, PAGE_SIZE);
> -
> - lim.max_hw_discard_sectors = max_bytes / block->bp_block;
> - lim.max_write_zeroes_sectors = lim.max_hw_discard_sectors;
> + if (device->discipline->disc_limits) {
> + device->discipline->disc_limits(block, &lim);
> + } else {
> + lim.discard_granularity = block->bp_block;
> + /* Calculate max_discard_sectors and make it PAGE aligned */
> + max_bytes = USHRT_MAX * block->bp_block;
> + max_bytes = ALIGN_DOWN(max_bytes, PAGE_SIZE);
> + lim.max_hw_discard_sectors = max_bytes / block->bp_block;
> + lim.max_write_zeroes_sectors = lim.max_hw_discard_sectors;
> + }
> }
[Severity: Medium]
If a volume is reconfigured from thin (ESE) to standard and re-evaluated,
does this leak stale discard limits?
Since queue_limits_start_update() retains the old limits, skipping an
explicit clearing of lim.max_hw_discard_sectors and lim.discard_granularity
when DASD_FEATURE_DISCARD is absent might leave stale values.
Could this cause the block layer to continue issuing REQ_OP_DISCARD requests
which are then silently dropped?
> rc = queue_limits_commit_update(block->gdp->queue, &lim);
> if (rc)
> return rc;
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
[ ... ]
> +static void dasd_eckd_disc_limits(struct dasd_block *block,
> + struct queue_limits *lim)
> +{
> + struct dasd_device *device = block->base;
> + struct dasd_eckd_private *private = device->private;
> + unsigned int logical_block_size = block->bp_block;
> + unsigned int max_discard_sectors, max_bytes, ext_bytes;
> + int recs_per_trk, trks_per_cyl, ext_limit, ext_size;
> +
> + if (!dasd_eckd_ese_capable(device) || dasd_eckd_ext_size(device) == 0)
> + return;
[Severity: Medium]
Similarly, does this early return abandon limit initialization without
clearing existing stale limits if the device is no longer ESE capable?
[ ... ]
> + lim->max_hw_discard_sectors = max_discard_sectors;
> + /*
> + * ext_bytes is the hardware extent size and is not a power of two, so
> + * the block layer's power-of-two round_up()/round_down() alignment
> + * helpers compute it only approximately. That is a hint, not a
> + * correctness requirement: RAS safety is enforced in the CCW builder,
> + * which rounds the range inward to whole extents and rejects a request
> + * that covers no whole extent, so a misaligned range is never
> + * over-released. At worst a few sub-extent discards are declined.
> + */
> + lim->discard_granularity = ext_bytes;
[Severity: High]
Is it safe to assign a non-power-of-two hardware extent size (ext_bytes)
to lim->discard_granularity here?
The block layer strictly requires this value to be a power of two because it
relies on bitwise masks in sysfs store functions, like
queue_max_discard_sectors_store(). Filesystems also expect a power of two
for these fields.
Could this corrupt bitwise calculations, randomly failing valid sysfs writes
and causing filesystems to miscalculate discard boundaries?
> +}
> +
> static struct ccw_driver dasd_eckd_driver = {
> .driver = {
> .name = "dasd-eckd",
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=17
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias
2026-08-01 11:10 ` [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias Stefan Haberland
@ 2026-08-01 11:46 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:46 UTC (permalink / raw)
To: Stefan Haberland
Cc: linux-s390, Vasily Gorbik, Alexander Gordeev, Heiko Carstens
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Premature `dasd_ft_bias_apply` call leaves adaptive heuristic variables uninitialized, causing FSM evaluation on every I/O
--
--- Patch [13]: [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias ---
commit 898e7c695e4b33bd0b7466d135dcbce8e6436652
Author: Stefan Haberland <sth@linux.ibm.com>
s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias
This patch derives an adaptive heuristic from the ft_bias configuration for
s390 DASD devices. It uses short probe windows to measure the NRF rate and
dynamically switches between fulltrack and plain writes, falling back and
capping the backoff interval as needed.
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 9003e28a4ebbe..67c301e182c0f 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
> @@ -2162,8 +2162,7 @@ dasd_eckd_check_characteristics(struct dasd_device *device)
>
> /* default ESE fulltrack write aggressiveness from the module parameter */
> device->ft_bias = min_t(unsigned int, full_track_bias, DASD_FT_BIAS_MAX);
> - /* only the "always" endpoint forces fulltrack unconditionally here */
> - device->fulltrack = (device->ft_bias >= DASD_FT_BIAS_MAX) ? 1 : 0;
> + dasd_ft_bias_apply(device);
[Severity: High]
Does calling dasd_ft_bias_apply() happen too early here, before the volume
information is read?
Because dasd_eckd_read_vol_info() hasn't been called yet, the ESE flag is
still 0, causing dasd_ese_adaptive() to return false during initialization.
This results in dasd_ft_bias_apply() returning early and leaving heuristic
parameters like ese_heu_start_interval and ese_heu_probe_window at 0:
drivers/s390/block/dasd_int.h:dasd_ft_bias_apply() {
...
if (!dasd_ese_adaptive(device)) {
device->fulltrack = (a >= DASD_FT_BIAS_MAX) ? 1 : 0;
device->ese_probe_state = DASD_ESE_HEU_FT1_ACTIVE;
return;
}
...
}
As a result, in dasd_ese_heuristic_tick(), the interval check will evaluate
to false on every I/O because ese_probe_interval is 0:
drivers/s390/block/dasd_eckd.c:dasd_ese_heuristic_tick() {
...
if (atomic_inc_return(&basedev->ese_io_cnt) < (int)basedev->ese_probe_interval)
return;
...
}
Will this completely defeat the backoff and cause the state machine to be
evaluated with atomic exchanges on every single I/O for the lifetime of the
device?
>
> if (private->conf.gneq) {
> value = 1;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=13
^ permalink raw reply [flat|nested] 37+ messages in thread
* Re: [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path
2026-08-01 11:10 ` [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path Stefan Haberland
@ 2026-08-01 11:49 ` sashiko-bot
0 siblings, 0 replies; 37+ messages in thread
From: sashiko-bot @ 2026-08-01 11:49 UTC (permalink / raw)
To: Stefan Haberland
Cc: Vasily Gorbik, Heiko Carstens, Alexander Gordeev, linux-s390
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
New issues:
- [Low] Missing READ_ONCE()/WRITE_ONCE() annotations for locklessly accessed cached variables.
Pre-existing issues:
- [High] Lockless CCW build paths read `uid.type` which is subject to a transient zero during configuration reload, resulting in malformed commands.
- [High] Race condition in `dasd_eckd_generate_uid` and `rebuild_device_uid` against configuration reload workers allowing Use-After-Free and kernel heap corruption.
- [High] Lockless access to `private->conf.data` in `dasd_eckd_fill_info` can lead to a Use-After-Free and leak freed kernel memory to userspace.
--
--- Patch [18]: [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path ---
commit db725aecd487c6d786378910088d76a932cca049
Author: Stefan Haberland <sth@linux.ibm.com>
Date: Sat Aug 1 11:10:08 2026 +0000
s390/dasd: Read cached unit address and LSS in the CCW build path
The CCW build path (prefix_LRE, the full-track prefix and dso_ras) read the
base address and LSS straight from conf.ned. That buffer is freed and
reallocated by the reload worker (do_reload_device - dasd_eckd_read_conf -
dasd_eckd_clear_conf_data), so a configuration change concurrent with I/O
can free conf.ned while a request is being built.
Use-after-free reported by KASAN in prefix_LRE.
Read the cached copies instead.
The unit address is already kept in uid.real_unit_addr, and the LSS is now
cached in ned_lss. Both are refreshed under the ccwdev lock in
dasd_eckd_generate_uid whenever the configuration is (re)read.
Also fix for prepare for read subsystem data (prssd) users.
Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
> diff --git a/drivers/s390/block/dasd_eckd.c b/drivers/s390/block/dasd_eckd.c
> index 09ef6d11f0ce7..816ea66fb2fe4 100644
> --- a/drivers/s390/block/dasd_eckd.c
> +++ b/drivers/s390/block/dasd_eckd.c
> @@ -588,8 +588,9 @@ static int prefix_LRE(struct ccw1 *ccw, struct PFX_eckd_data *pfxdata,
> return -EINVAL;
> }
> pfxdata->format = format;
> - pfxdata->base_address = basepriv->conf.ned->unit_addr;
> - pfxdata->base_lss = basepriv->conf.ned->ID;
> + /* cached copies - conf.ned may be freed under us by the reload worker */
> + pfxdata->base_address = basepriv->ned_ua;
> + pfxdata->base_lss = basepriv->ned_lss;
[Severity: Low]
Are READ_ONCE() and WRITE_ONCE() annotations needed for these cached
variables?
The variables private->ned_lss and private->ned_ua are updated in
dasd_eckd_generate_uid() under the ccwdev_lock, but are intentionally read
locklessly here in prefix_LRE().
The Linux kernel memory model typically requires READ_ONCE() and WRITE_ONCE()
for concurrent lockless accesses to prevent compiler optimizations like load
fusing and to document the intentional data race, which avoids KCSAN
warnings.
> pfxdata->validity.define_extent = 1;
>
> /* private uid is kept up to date, conf_data may be outdated */
[Severity: High]
This is a pre-existing issue, but does this lockless read of
startpriv->uid.type risk seeing a transient zero?
Just after this comment, prefix_LRE() and prepare_itcw() locklessly read
startpriv->uid.type. When a reload worker concurrently calls create_uid(),
it memsets the entire uid structure to zero:
drivers/s390/block/dasd_eckd.c:create_uid() {
...
memset(uid, 0, sizeof(struct dasd_uid));
...
}
If prefix_LRE() or prepare_itcw() reads startpriv->uid.type while it is
transiently zeroed, the reader may observe the type as 0 (UA_BASE_DEVICE)
instead of UA_BASE_PAV_ALIAS or UA_HYPER_PAV_ALIAS.
Consequently, the driver will silently fail to set the verify_base and
hyper_pav flags in the PFX hardware command, which could lead to malformed
storage requests, rejected I/Os, or potential data corruption.
[ ... ]
> @@ -806,6 +807,9 @@ static int dasd_eckd_generate_uid(struct dasd_device *device)
> return -ENODEV;
> spin_lock_irqsave(get_ccwdev_lock(device->cdev), flags);
> create_uid(&private->conf, &private->uid);
> + /* cache LSS and unit address for the lockless CCW-build path */
> + private->ned_lss = private->conf.ned->ID;
> + private->ned_ua = private->conf.ned->unit_addr;
> spin_unlock_irqrestore(get_ccwdev_lock(device->cdev), flags);
> return 0;
> }
[Severity: High]
This is a pre-existing issue, but is there a use-after-free risk involving
private->conf.ned in this function?
Just before the lock is acquired, dasd_eckd_generate_uid() locklessly checks:
drivers/s390/block/dasd_eckd.c:dasd_eckd_generate_uid() {
...
if (!private->conf.ned || !private->conf.gneq)
return -ENODEV;
spin_lock_irqsave(...)
...
}
Concurrently, a reload worker executing dasd_eckd_read_conf() calls
dasd_eckd_clear_conf_data(), which frees the underlying conf.data memory
without acquiring ccwdev_lock.
Since conf.ned points inside the freed conf.data allocation,
dasd_eckd_generate_uid() can pass the lockless check but dereference freed
memory once inside the lock.
Additionally, rebuild_device_uid() writes directly to private->conf.data
using memcpy() without locking. If the reload worker frees conf.data
concurrently, rebuild_device_uid() might overwrite freed slab memory, causing
kernel heap corruption.
[Severity: High]
This is a pre-existing issue, but does dasd_eckd_fill_info() also risk a
use-after-free that could leak freed memory to userspace?
The BIODASDINFO ioctl path calls dasd_ioctl_information(), which invokes
dasd_eckd_fill_info() without acquiring any device locks:
drivers/s390/block/dasd_eckd.c:dasd_eckd_fill_info() {
...
memcpy(info->configuration_data, private->conf.data, info->confdata_size);
...
}
Concurrently, a reload worker executing dasd_eckd_reload_device() calls
dasd_eckd_clear_conf_data(), setting private->conf.data to NULL and freeing
the underlying memory:
drivers/s390/block/dasd_eckd.c:dasd_eckd_clear_conf_data() {
...
private->conf.data = NULL;
...
kfree(device->path[i].conf_data);
}
If the ioctl thread reads the private->conf.data pointer just before the
reload worker frees it, the memcpy() will read from freed slab memory, leaking
potentially sensitive kernel data to userspace. If it reads NULL, it will
cause a kernel panic.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260801111008.3391031-1-sth@linux.ibm.com?part=18
^ permalink raw reply [flat|nested] 37+ messages in thread
end of thread, other threads:[~2026-08-01 11:49 UTC | newest]
Thread overview: 37+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-01 11:09 [PATCH v6 00/18] s390/dasd: ESE thin-provisioning performance improvements Stefan Haberland
2026-08-01 11:09 ` [PATCH v6 01/18] s390/dasd: Do not complete a failed ESE read as successful Stefan Haberland
2026-08-01 11:27 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 02/18] s390/dasd: Propagate partial completion length across ERP recovery Stefan Haberland
2026-08-01 11:21 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 03/18] s390/dasd: Guard sysfs discipline callbacks against unallocated private data Stefan Haberland
2026-08-01 11:40 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 04/18] s390/dasd: Optimize max blocks per request for track alignment Stefan Haberland
2026-08-01 11:20 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 05/18] s390/dasd: Use GFP_KERNEL in dasd_alloc_device() Stefan Haberland
2026-08-01 11:21 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 06/18] s390/dasd: Add defines for the Extended Address Volume track address Stefan Haberland
2026-08-01 11:13 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 07/18] s390/dasd: Add infrastructure for ESE full-track write Stefan Haberland
2026-08-01 11:32 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 08/18] s390/dasd: Add range-based format-track collision detection Stefan Haberland
2026-08-01 11:36 ` sashiko-bot
2026-08-01 11:09 ` [PATCH v6 09/18] s390/dasd: Extend prepare_itcw() to support WRITE_FULL_TRACK Stefan Haberland
2026-08-01 11:37 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 10/18] s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack() Stefan Haberland
2026-08-01 11:29 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 11/18] s390/dasd: Use WRITE_FULL_TRACK in ESE format handler Stefan Haberland
2026-08-01 11:39 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 12/18] s390/dasd: Add full_track_bias to control fulltrack write mode Stefan Haberland
2026-08-01 11:27 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 13/18] s390/dasd: Derive adaptive ESE fulltrack heuristic from ft_bias Stefan Haberland
2026-08-01 11:46 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 14/18] s390/dasd: Stamp a format label into newly formatted volumes Stefan Haberland
2026-08-01 11:33 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 15/18] s390/dasd: Detect ESE volumes from the on-disk format label Stefan Haberland
2026-08-01 11:38 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 16/18] s390/dasd: Report ESE capability and format mode at device online Stefan Haberland
2026-08-01 11:45 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 17/18] s390/dasd: Re-enable discard support for ESE volumes Stefan Haberland
2026-08-01 11:45 ` sashiko-bot
2026-08-01 11:10 ` [PATCH v6 18/18] s390/dasd: Read cached unit address and LSS in the CCW build path Stefan Haberland
2026-08-01 11:49 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox