[PATCH 0/3] ublk: validate zoned SET_PARAMS earlier

STALE65d

6 messages, 3 authors, 2026-08-16 · open the first message on its own page

[PATCH 0/3] ublk: validate zoned SET_PARAMS earlier

From: Yao Sang <hidden>
Date: 2026-08-14 02:33:19

Hi,

UBLK_F_ZONED uses params.basic.chunk_sectors as zone size. ublk also
uses it to calculate number of zones with ilog2().

Before this series, SET_PARAMS can accept non-power-of-2 zone size.
Then the device fails later when it is started. Reject it in SET_PARAMS
and add one selftest for this kind of parameter checks.

Patch 1 adds the missing check.

Patch 2 adds a kublk helper which sends SET_PARAMS without START_DEV.

Patch 3 adds test_params_01.sh. It covers basic parameter checks and
zoned parameter checks, including the non-power-of-2 zone size case.

Without patch 1, the non-power-of-2 zoned case is accepted by
SET_PARAMS. With patch 1, it fails in SET_PARAMS.

Yao Sang (3):
  ublk: reject non-power-of-2 zone sizes in SET_PARAMS
  selftests: ublk: add helper for SET_PARAMS
  selftests: ublk: add SET_PARAMS validation test

 drivers/block/ublk_drv.c                      |   2 +-
 tools/testing/selftests/ublk/Makefile         |   2 +
 tools/testing/selftests/ublk/kublk.c          | 161 +++++++++++++++++-
 tools/testing/selftests/ublk/kublk.h          |  18 ++
 .../testing/selftests/ublk/test_params_01.sh  | 114 +++++++++++++
 5 files changed, 295 insertions(+), 2 deletions(-)
 create mode 100755 tools/testing/selftests/ublk/test_params_01.sh

-- 
2.25.1

[PATCH v1 1/3] ublk: reject non-power-of-2 zone sizes in SET_PARAMS

From: Yao Sang <hidden>
Date: 2026-08-14 02:33:22

UBLK_F_ZONED uses params.basic.chunk_sectors as zone size. ublk uses
ilog2(chunk_sectors) to get number of zones, so the value must be power
of 2.

If chunk_sectors is 96 and dev_sectors is 96 * 16, userspace asks for
16 zones. But the shift calculation gets 24 zones.

Block layer rejects such zone size when the disk is started. But
SET_PARAMS has already returned success, which is confusing for
userspace. Reject it in SET_PARAMS with other zoned parameter checks.

Fixes: 29802d7ca33b ("ublk: enable zoned storage support")
Signed-off-by: Yao Sang <redacted>
---
 drivers/block/ublk_drv.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/block/ublk_drv.c b/drivers/block/ublk_drv.c
index 853cd40a3403..193f6ebc3b37 100644
--- a/drivers/block/ublk_drv.c
+++ b/drivers/block/ublk_drv.c
@@ -980,7 +980,7 @@ static int ublk_validate_params(const struct ublk_device *ub)
 		if (p->max_sectors < PAGE_SECTORS)
 			return -EINVAL;
 
-		if (ublk_dev_is_zoned(ub) && !p->chunk_sectors)
+		if (ublk_dev_is_zoned(ub) && !is_power_of_2(p->chunk_sectors))
 			return -EINVAL;
 	} else
 		return -EINVAL;
-- 
2.25.1

[PATCH v1 2/3] selftests: ublk: add helper for SET_PARAMS

From: Yao Sang <hidden>
Date: 2026-08-14 02:33:53

The normal kublk add command goes through device startup. It does not
tell the shell test whether a bad parameter is rejected by SET_PARAMS or
later by START_DEV.

Add a set_params command. It creates a temporary ublk device, sends
SET_PARAMS with the command line parameters, returns the ioctl result,
and deletes the device before START_DEV.

Signed-off-by: Yao Sang <redacted>
---
 tools/testing/selftests/ublk/kublk.c | 161 ++++++++++++++++++++++++++-
 tools/testing/selftests/ublk/kublk.h |  18 +++
 2 files changed, 178 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/ublk/kublk.c b/tools/testing/selftests/ublk/kublk.c
index be5a0d775952..2400b4615766 100644
--- a/tools/testing/selftests/ublk/kublk.c
+++ b/tools/testing/selftests/ublk/kublk.c
@@ -8,6 +8,13 @@
 #include "kublk.h"
 
 #define MAX_NR_TGT_ARG 	64
+#define KUBLK_PARAM_LOGICAL_BS_SHIFT		9
+#define KUBLK_PARAM_PHYSICAL_BS_SHIFT		12
+#define KUBLK_PARAM_ZONE_SECTORS		128
+#define KUBLK_PARAM_NR_ZONES			16
+#define KUBLK_PARAM_DEV_SECTORS			\
+	(KUBLK_PARAM_ZONE_SECTORS * KUBLK_PARAM_NR_ZONES)
+#define KUBLK_PARAM_ZONE_APPEND_SECTORS		8
 
 unsigned int ublk_dbg_mask = UBLK_LOG;
 static const struct ublk_tgt_ops *tgt_ops_list[] = {
@@ -227,6 +234,55 @@ static int ublk_ctrl_get_features(struct ublk_dev *dev,
 	return __ublk_ctrl_cmd(dev, &data);
 }
 
+static int parse_param_types(const char *arg, __u32 *types)
+{
+	char buf[128], *save = NULL, *tok;
+
+	if (strlen(arg) >= sizeof(buf))
+		return -EINVAL;
+
+	strcpy(buf, arg);
+	*types = 0;
+	tok = strtok_r(buf, ",", &save);
+	while (tok) {
+		if (!strcmp(tok, "none"))
+			;
+		else if (!strcmp(tok, "basic"))
+			*types |= UBLK_PARAM_TYPE_BASIC;
+		else if (!strcmp(tok, "zoned"))
+			*types |= UBLK_PARAM_TYPE_ZONED;
+		else
+			return -EINVAL;
+		tok = strtok_r(NULL, ",", &save);
+	}
+
+	return 0;
+}
+
+static void ublk_init_params_from_ctx(const struct dev_ctx *ctx,
+				      struct ublk_params *params)
+{
+	const struct params_ctx *p = &ctx->params;
+
+	*params = (struct ublk_params) {
+		.types = p->types,
+		.basic = {
+			.logical_bs_shift	= p->logical_bs_shift,
+			.physical_bs_shift	= p->physical_bs_shift,
+			.io_min_shift		= p->io_min_shift,
+			.io_opt_shift		= p->io_opt_shift,
+			.max_sectors		= p->max_sectors,
+			.chunk_sectors		= p->chunk_sectors,
+			.dev_sectors		= p->dev_sectors,
+		},
+		.zoned = {
+			.max_open_zones		= p->max_open_zones,
+			.max_active_zones	= p->max_active_zones,
+			.max_zone_append_sectors = p->max_zone_append_sectors,
+		},
+	};
+}
+
 static int ublk_ctrl_update_size(struct ublk_dev *dev,
 		__u64 nr_sects)
 {
@@ -1772,6 +1828,51 @@ static int __cmd_dev_add(const struct dev_ctx *ctx)
 
 static int __cmd_dev_list(struct dev_ctx *ctx);
 
+static int cmd_dev_set_params(struct dev_ctx *ctx)
+{
+	struct ublksrv_ctrl_dev_info *info;
+	struct ublk_params params;
+	struct ublk_dev *dev;
+	__u64 features;
+	int ret, del_ret;
+
+	dev = ublk_ctrl_init();
+	if (!dev)
+		return -ENODEV;
+
+	ret = ublk_ctrl_get_features(dev, &features);
+	if (ret < 0)
+		goto out;
+
+	if (!(features & UBLK_F_CMD_IOCTL_ENCODE)) {
+		ret = -ENOTSUP;
+		goto out;
+	}
+
+	info = &dev->dev_info;
+	info->dev_id = ctx->dev_id;
+	info->nr_hw_queues = ctx->nr_hw_queues;
+	info->queue_depth = ctx->queue_depth;
+	info->io_desc_size = ctx->io_desc_size;
+	info->flags = ctx->flags;
+
+	ret = ublk_ctrl_add_dev(dev);
+	if (ret < 0)
+		goto out;
+
+	ublk_init_params_from_ctx(ctx, &params);
+
+	ret = ublk_ctrl_set_params(dev, &params);
+	printf("SET_PARAMS returned %d\n", ret);
+
+	del_ret = ublk_ctrl_del_dev(dev);
+	if (del_ret < 0 && ret == 0)
+		ret = del_ret;
+out:
+	ublk_ctrl_deinit(dev);
+	return ret < 0 ? ret : 0;
+}
+
 static int cmd_dev_add(struct dev_ctx *ctx)
 {
 	int res;
@@ -2117,6 +2218,9 @@ static int cmd_dev_help(char *exe)
 	printf("\t --safe only stop if device has no active openers\n\n");
 	printf("%s list [-n dev_id] -a \n", exe);
 	printf("\t -a list all devices, -n list specified device, default -a \n\n");
+	printf("%s set_params [-n dev_id] [-q nr_queues] [-d depth] [-u] [--zoned]\n", exe);
+	printf("\t[--param_types basic[,zoned]|none]\n");
+	printf("\t issue ADD_DEV, SET_PARAMS and DEL_DEV without START_DEV\n\n");
 	printf("%s features\n", exe);
 	printf("%s update_size -n dev_id -s|--size size_in_bytes \n", exe);
 	printf("%s quiesce -n dev_id\n", exe);
@@ -2160,6 +2264,18 @@ int main(int argc, char *argv[])
 		{ "htlb",		1,	NULL,  0  },
 		{ "rdonly_shmem_buf",	0,	NULL,  0  },
 		{ "io_desc_size",	1,	NULL,  0  },
+		{ "zoned",		0,	NULL,  0  },
+		{ "param_types",	1,	NULL,  0  },
+		{ "logical_bs_shift",	1,	NULL,  0  },
+		{ "physical_bs_shift",	1,	NULL,  0  },
+		{ "io_min_shift",	1,	NULL,  0  },
+		{ "io_opt_shift",	1,	NULL,  0  },
+		{ "max_sectors",	1,	NULL,  0  },
+		{ "chunk_sectors",	1,	NULL,  0  },
+		{ "dev_sectors",	1,	NULL,  0  },
+		{ "max_zone_append_sectors", 1, NULL,  0  },
+		{ "max_open_zones",	1,	NULL,  0  },
+		{ "max_active_zones",	1,	NULL,  0  },
 		{ 0, 0, 0, 0 }
 	};
 	const struct ublk_tgt_ops *ops = NULL;
@@ -2173,6 +2289,19 @@ int main(int argc, char *argv[])
 		.tgt_type	=	"unknown",
 		.csum_type	=	LBMD_PI_CSUM_NONE,
 		.io_desc_size	=	sizeof(struct ublksrv_io_desc),
+		.params = {
+			.types			= UBLK_PARAM_TYPE_BASIC,
+			.logical_bs_shift	= KUBLK_PARAM_LOGICAL_BS_SHIFT,
+			.physical_bs_shift	= KUBLK_PARAM_PHYSICAL_BS_SHIFT,
+			.io_min_shift		= KUBLK_PARAM_LOGICAL_BS_SHIFT,
+			.io_opt_shift		= KUBLK_PARAM_PHYSICAL_BS_SHIFT,
+			.max_sectors		=
+				UBLK_IO_MAX_BYTES >> KUBLK_PARAM_LOGICAL_BS_SHIFT,
+			.chunk_sectors		= KUBLK_PARAM_ZONE_SECTORS,
+			.dev_sectors		= KUBLK_PARAM_DEV_SECTORS,
+			.max_zone_append_sectors =
+				KUBLK_PARAM_ZONE_APPEND_SECTORS,
+		},
 	};
 	int ret = -EINVAL, i;
 	int tgt_argc = 1;
@@ -2288,6 +2417,34 @@ int main(int argc, char *argv[])
 				ctx.flags |= UBLK_F_IO_DESC_SIZE;
 				ctx.io_desc_size = strtoul(optarg, NULL, 0);
 			}
+			if (!strcmp(longopts[option_idx].name, "zoned"))
+				ctx.flags |= UBLK_F_ZONED;
+			if (!strcmp(longopts[option_idx].name, "param_types")) {
+				ret = parse_param_types(optarg, &ctx.params.types);
+				if (ret)
+					return ret;
+			}
+			if (!strcmp(longopts[option_idx].name, "logical_bs_shift"))
+				ctx.params.logical_bs_shift = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "physical_bs_shift"))
+				ctx.params.physical_bs_shift = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "io_min_shift"))
+				ctx.params.io_min_shift = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "io_opt_shift"))
+				ctx.params.io_opt_shift = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "max_sectors"))
+				ctx.params.max_sectors = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "chunk_sectors"))
+				ctx.params.chunk_sectors = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "dev_sectors"))
+				ctx.params.dev_sectors = strtoull(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "max_zone_append_sectors"))
+				ctx.params.max_zone_append_sectors =
+					strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "max_open_zones"))
+				ctx.params.max_open_zones = strtoul(optarg, NULL, 0);
+			if (!strcmp(longopts[option_idx].name, "max_active_zones"))
+				ctx.params.max_active_zones = strtoul(optarg, NULL, 0);
 			break;
 		case '?':
 			/*
@@ -2377,7 +2534,9 @@ int main(int argc, char *argv[])
 		ops->parse_cmd_line(&ctx, tgt_argc, tgt_argv);
 	}
 
-	if (!strcmp(cmd, "add"))
+	if (!strcmp(cmd, "set_params"))
+		ret = cmd_dev_set_params(&ctx);
+	else if (!strcmp(cmd, "add"))
 		ret = cmd_dev_add(&ctx);
 	else if (!strcmp(cmd, "recover")) {
 		if (ctx.dev_id < 0) {
diff --git a/tools/testing/selftests/ublk/kublk.h b/tools/testing/selftests/ublk/kublk.h
index e27c154fc910..d98f3d612d88 100644
--- a/tools/testing/selftests/ublk/kublk.h
+++ b/tools/testing/selftests/ublk/kublk.h
@@ -63,6 +63,22 @@ struct fault_inject_ctx {
 	bool die_during_fetch;
 };
 
+struct params_ctx {
+	__u32 types;
+
+	__u32 logical_bs_shift;
+	__u32 physical_bs_shift;
+	__u32 io_min_shift;
+	__u32 io_opt_shift;
+	__u32 max_sectors;
+	__u32 chunk_sectors;
+	__u64 dev_sectors;
+
+	__u32 max_open_zones;
+	__u32 max_active_zones;
+	__u32 max_zone_append_sectors;
+};
+
 struct dev_ctx {
 	char tgt_type[16];
 	unsigned long flags;
@@ -99,6 +115,8 @@ struct dev_ctx {
 	/* for 'update_size' command */
 	unsigned long long size;
 
+	struct params_ctx params;
+
 	char *htlb_path;
 
 	union {
-- 
2.25.1

[PATCH v1 3/3] selftests: ublk: add SET_PARAMS validation test

From: Yao Sang <hidden>
Date: 2026-08-14 02:33:55

Add test_params_01.sh for SET_PARAMS. The test checks valid basic
parameters and several invalid parameter cases.

Also cover zoned parameters, including a non-power-of-2 zone size. This
case must fail in SET_PARAMS instead of being accepted and rejected later
when the device is started.

Signed-off-by: Yao Sang <redacted>
---
 tools/testing/selftests/ublk/Makefile         |   2 +
 .../testing/selftests/ublk/test_params_01.sh  | 114 ++++++++++++++++++
 2 files changed, 116 insertions(+)
 create mode 100755 tools/testing/selftests/ublk/test_params_01.sh
diff --git a/tools/testing/selftests/ublk/Makefile b/tools/testing/selftests/ublk/Makefile
index a3cec7b35db7..5daf36c6c36c 100644
--- a/tools/testing/selftests/ublk/Makefile
+++ b/tools/testing/selftests/ublk/Makefile
@@ -55,6 +55,8 @@ TEST_PROGS += test_stripe_06.sh
 TEST_PROGS += test_part_01.sh
 TEST_PROGS += test_part_02.sh
 
+TEST_PROGS += test_params_01.sh
+
 TEST_PROGS += test_shmemzc_01.sh
 TEST_PROGS += test_shmemzc_02.sh
 TEST_PROGS += test_shmemzc_03.sh
diff --git a/tools/testing/selftests/ublk/test_params_01.sh b/tools/testing/selftests/ublk/test_params_01.sh
new file mode 100755
index 000000000000..928e72b1035d
--- /dev/null
+++ b/tools/testing/selftests/ublk/test_params_01.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+. "$(cd "$(dirname "$0")" && pwd)"/test_common.sh
+
+ERR_CODE=0
+
+run_set_params_success()
+{
+	local name=$1
+
+	shift
+
+	echo "$name"
+	if ! "$UBLK_PROG" set_params -q 1 -d 2 "$@"; then
+		echo "$name: SET_PARAMS check failed"
+		return 1
+	fi
+}
+
+run_set_params_failure()
+{
+	local name=$1
+
+	shift
+
+	echo "$name"
+	if "$UBLK_PROG" set_params -q 1 -d 2 "$@"; then
+		echo "$name: SET_PARAMS succeeded unexpectedly"
+		return 1
+	fi
+}
+
+run_zoned_set_params_success()
+{
+	local name=$1
+
+	shift
+
+	echo "$name"
+	if ! "$UBLK_PROG" set_params -q 1 -d 2 -u --zoned "$@"; then
+		echo "$name: SET_PARAMS check failed"
+		return 1
+	fi
+}
+
+run_zoned_set_params_failure()
+{
+	local name=$1
+
+	shift
+
+	echo "$name"
+	if "$UBLK_PROG" set_params -q 1 -d 2 -u --zoned "$@"; then
+		echo "$name: SET_PARAMS succeeded unexpectedly"
+		return 1
+	fi
+}
+
+_prep_test "params" "SET_PARAMS validation"
+
+if [ ! -c /dev/ublk-control ]; then
+	_cleanup_test
+	_show_result $TID $UBLK_SKIP_CODE
+fi
+
+run_set_params_success "valid basic params" ||
+	ERR_CODE=1
+
+run_set_params_failure "missing basic params" \
+	--param_types none ||
+	ERR_CODE=1
+
+run_set_params_failure "logical block larger than physical block" \
+	--logical_bs_shift 12 --physical_bs_shift 9 ||
+	ERR_CODE=1
+
+run_set_params_failure "too large max sectors" \
+	--max_sectors 2049 ||
+	ERR_CODE=1
+
+if _have_feature "ZONED" && _have_feature "USER_COPY"; then
+	run_zoned_set_params_success "valid zoned params" \
+		--param_types basic,zoned ||
+		ERR_CODE=1
+
+	run_zoned_set_params_failure "missing zoned params" ||
+		ERR_CODE=1
+
+	run_zoned_set_params_failure "non-power-of-2 zone size" \
+		--param_types basic,zoned \
+		--chunk_sectors 96 --dev_sectors $((96 * 16)) ||
+		ERR_CODE=1
+
+	run_zoned_set_params_failure "zero max zone append" \
+		--param_types basic,zoned \
+		--max_zone_append_sectors 0 ||
+		ERR_CODE=1
+
+	run_zoned_set_params_failure "too many open zones" \
+		--param_types basic,zoned \
+		--dev_sectors $((128 * 4)) --max_open_zones 5 ||
+		ERR_CODE=1
+
+	run_zoned_set_params_failure "too many active zones" \
+		--param_types basic,zoned \
+		--dev_sectors $((128 * 4)) --max_active_zones 5 ||
+		ERR_CODE=1
+else
+	echo "zoned ublk feature unavailable, skip zoned SET_PARAMS cases"
+fi
+
+_cleanup_test
+_show_result $TID $ERR_CODE
-- 
2.25.1

Re: [PATCH v1 1/3] ublk: reject non-power-of-2 zone sizes in SET_PARAMS

From: Ming Lei <tom.leiming@gmail.com>
Date: 2026-08-14 10:07:28

On Thu, Aug 13, 2026 at 9:33 PM Yao Sang [off-list ref] wrote:
quoted hunk
UBLK_F_ZONED uses params.basic.chunk_sectors as zone size. ublk uses
ilog2(chunk_sectors) to get number of zones, so the value must be power
of 2.

If chunk_sectors is 96 and dev_sectors is 96 * 16, userspace asks for
16 zones. But the shift calculation gets 24 zones.

Block layer rejects such zone size when the disk is started. But
SET_PARAMS has already returned success, which is confusing for
userspace. Reject it in SET_PARAMS with other zoned parameter checks.

Fixes: 29802d7ca33b ("ublk: enable zoned storage support")
Signed-off-by: Yao Sang <redacted>
---
 drivers/block/ublk_drv.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/block/ublk_drv.c b/drivers/block/ublk_drv.c
index 853cd40a3403..193f6ebc3b37 100644
--- a/drivers/block/ublk_drv.c
+++ b/drivers/block/ublk_drv.c
@@ -980,7 +980,7 @@ static int ublk_validate_params(const struct ublk_device *ub)
                if (p->max_sectors < PAGE_SECTORS)
                        return -EINVAL;

-               if (ublk_dev_is_zoned(ub) && !p->chunk_sectors)
+               if (ublk_dev_is_zoned(ub) && !is_power_of_2(p->chunk_sectors))
Looks fine,  and is_power_of_2() returns false for 0.

Thanks,
Ming Lei

Re: [PATCH 0/3] ublk: validate zoned SET_PARAMS earlier

From: Jens Axboe <axboe@kernel.dk>
Date: 2026-08-16 00:01:54

On Fri, 14 Aug 2026 10:32:23 +0800, Yao Sang wrote:
UBLK_F_ZONED uses params.basic.chunk_sectors as zone size. ublk also
uses it to calculate number of zones with ilog2().

Before this series, SET_PARAMS can accept non-power-of-2 zone size.
Then the device fails later when it is started. Reject it in SET_PARAMS
and add one selftest for this kind of parameter checks.

[...]
Applied, thanks!

[1/3] ublk: reject non-power-of-2 zone sizes in SET_PARAMS
      commit: 2707acf1856da266139986c9398ad722fd4f48c0
[2/3] selftests: ublk: add helper for SET_PARAMS
      commit: 176f02a86c0384df4ba175e319321b07089a8016
[3/3] selftests: ublk: add SET_PARAMS validation test
      commit: 007af5e5cc822aed3a0909534ffb810f7816ed1f

Best regards,
-- 
Jens Axboe


Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help