Linux RAID subsystem development
 help / color / mirror / Atom feed
From: Abd-Alrhman Masalkhi <abd.masalkhi@gmail.com>
To: sashiko-reviews@lists.linux.dev
Cc: linux-raid@vger.kernel.org, yukuai@fygo.io
Subject: Re: [RFC PATCH v5 4/4] md: add a control device for array management
Date: Fri, 30 Oct 2026 09:35:52 +0100	[thread overview]
Message-ID: <m2wlqz1t7b.fsf@gmail.com> (raw)
In-Reply-To: <sashiko-outbox-189939@kernel.org>

Hi Kuai,

On Thu, Oct 29, 2026 at 20:48 +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
> - [Critical] md: unlocked set_disk_faulty() call in mdctl_ioctl() races with __md_stop()
> - [High] md: missing MD_CLOSING check in mdctl_find_mddev() and mdctl_ioctl()
> - [High] md: missing state revalidation after lock drop in mdctl_ioctl()
> - [Medium] md: MD_CTL_STOP_ARRAY with MD_CTL_RO_FLAG returns -EINVAL on inactive array
> - [Medium] md: mdctl_get_mddev_uuid() and mdctl_lookup_array() use non-native or stale uuid
> - [Medium] md: tautological MINOR() check and u64 md_dev truncation in mdctl_find_mddev()
> - [Medium] md: race between md_alloc() kobject_add() failure and mdctl_find_mddev()
> --
>
> commit a0c97489b840f0bb7566f8fcc5073f039699c2e5
> Author: Abd-Alrhman Masalkhi <abd.masalkhi@gmail.com>
>
> md: add a control device for array management
>
> This commit introduces a /dev/md-control misc device to allow configuring,
> querying, and stopping MD arrays by name, UUID, or device number without
> opening the MD block device directly. It also moves disks_mutex to file
> scope to synchronize control device lookups with md_alloc().
>
>> diff --git a/drivers/md/md.c b/drivers/md/md.c
>> index 3b4497b615cf0..a1425b5e6b3ec 100644
>> --- a/drivers/md/md.c
>> +++ b/drivers/md/md.c
>
> [ ... ]
>
>> @@ -8601,6 +8607,618 @@ static int md_compat_ioctl(struct block_device *bdev, blk_mode_t mode,
>>  }
>>  #endif /* CONFIG_COMPAT */
>>  
>> +static struct mddev *mdctl_get_mddev_dev(dev_t dev)
>> +{
>> +	struct mddev *mddev, *ret = ERR_PTR(-EINVAL);
>> +
>> +	if (MINOR(dev) >= (1 << MINORBITS))
>> +		return ret;
>
> [ ... ]
>
>> +static struct mddev *mdctl_get_mddev_uuid(const char *uuid)
>> +{
>> +	struct mddev *mddev, *ret = ERR_PTR(-ENODEV);
>> +
>> +	spin_lock(&all_mddevs_lock);
>> +	list_for_each_entry(mddev, &all_mddevs, all_mddevs) {
>> +		if (memcmp(mddev->uuid, uuid, sizeof(mddev->uuid)) == 0) {
>> +			ret = mddev_get(mddev);
>> +			if (!ret)
>> +				ret = ERR_PTR(-ENODEV);
>> +			break;
>> +		}
>> +	}
>> +	spin_unlock(&all_mddevs_lock);
>> +
>> +	return ret;
>> +}
>
> [Severity: Medium]
> Can mdctl_get_mddev_uuid() and mdctl_lookup_array() match or report a
> non-native or stale mddev->uuid?
>
> The commit description notes that UUID lookup is only available for arrays
> with native metadata, but there are two places where mddev->uuid is
> populated on arrays that do not have active persistent native metadata:
>
> 1. In md_set_array_info(), get_random_bytes(mddev->uuid, 16) populates
>    mddev->uuid even when info->not_persistent is set (!mddev->persistent).
>    As a result, mdctl_get_mddev_uuid() matches non-persistent arrays and
>    mdctl_lookup_array() copies a non-zero UUID to userspace for them.
>

In the case of external metadata, wouldn't the internal UUID differ
from the real one? Shouldn't get_random_bytes() be called only for
native metadata?

> 2. When an array is stopped via do_md_stop() -> md_clean(), md_clean()
>    resets mddev->raid_disks and mddev->persistent to 0, but never clears
>    mddev->uuid:
>
> drivers/md/md.c:md_clean() {
> 	mddev->array_sectors = 0;
> 	...
> 	mddev->raid_disks = 0;
> 	...
> 	mddev->persistent = 0;
> }
>
> If the stopped mddev remains in all_mddevs (for example, while
> hold_active == UNTIL_STOP or before delayed deletion completes),
> mdctl_get_mddev_uuid() will still match its stale mddev->uuid. Furthermore,
> if an older mddev with MD_DELETED set is still on all_mddevs when the array
> is re-assembled on a new mddev later in the list, mddev_get() will return
> NULL on the first match and mdctl_get_mddev_uuid() will break out of the
> loop and return -ENODEV instead of continuing to find the live mddev.
>
> Should mdctl_get_mddev_uuid() and mdctl_lookup_array() check
> mddev->persistent (and skip entries where mddev_get() fails), or should
> md_clean() also clear mddev->uuid?
>
>> +
>> +static struct mddev *mdctl_find_mddev(struct mdu_ioctl *md_ctl)
>> +{
>> +	struct mddev *ret = ERR_PTR(-EINVAL);
>> +
>> +	/*
>> +	 * md_alloc() adds a new mddev to all_mddevs before it creates the
>> +	 * gendisk. If creation fails. it frees the mddev directly, without
>> +	 * looking at ->active. All of this happens under disks_mutex.
>> +	 * Holding disks_mutex for the lookup means we only see mddevs that
>> +	 * md_alloc() has finished, so taking a reference is safe and every
>> +	 * mddev we find has a gendisk.
>> +	 */
>> +	mutex_lock(&disks_mutex);
>
> [Severity: Medium]
> Does holding disks_mutex in mdctl_find_mddev() prevent taking a reference to
> an mddev when kobject_add() fails in md_alloc()?
>
> In md_alloc(), if kobject_add() fails, disks_mutex is unlocked before
> mddev_put() is called:
>
> drivers/md/md.c:md_alloc() {
> 	...
> 	error = kobject_add(&mddev->kobj, &disk_to_dev(disk)->kobj, "%s", "md");
> 	if (error) {
> 		mddev->hold_active = 0;
> 		mutex_unlock(&disks_mutex);
> 		mddev_put(mddev);
> 		return ERR_PTR(error);
> 	}
> }
>
> Because MD_DELETED is only set inside mddev_put() -> __mddev_put() when
> mddev->active drops to 0, a concurrent mdctl_ioctl() -> mdctl_find_mddev()
> can acquire disks_mutex right after md_alloc() unlocks it and call
> mddev_get(mddev), incrementing mddev->active from 1 to 2 before md_alloc()
> calls mddev_put(mddev).
>
> When md_alloc() then calls mddev_put(mddev), atomic_dec_and_lock() sees
> mddev->active drop from 2 to 1 and returns early without setting MD_DELETED.
> If the racing mdctl_ioctl() runs MD_CTL_SET_ARRAY_INFO, md_set_array_info()
> sets mddev->ctime (and mddev->raid_disks), causing the subsequent
> mddev_put() in mdctl_ioctl() to also skip __mddev_put() deletion and leaving
> an mddev without a sysfs kobject in all_mddevs.
>

Yes, i will fix this in the next version.

>> +
>> +	if (memchr_inv(md_ctl->md_uuid, 0, MD_UUID_LEN)) {
>> +		if (*md_ctl->md_name || md_ctl->md_dev) {
>> +			pr_err_ratelimited("md: only supply one of name, uuid or dev number\n");
>> +			goto out;
>> +		}
>> +		ret = mdctl_get_mddev_uuid(md_ctl->md_uuid);
>> +	} else if (*md_ctl->md_name) {
>> +		if (md_ctl->md_dev) {
>> +			pr_err_ratelimited("md: only supply one of name, uuid or dev number\n");
>> +			goto out;
>> +		}
>> +		ret = mdctl_get_mddev_name(md_ctl->md_name);
>> +	} else if (md_ctl->md_dev) {
>> +		ret = mdctl_get_mddev_dev(new_decode_dev(md_ctl->md_dev));
>> +	}
>
> [Severity: Medium]
> Can passing md_ctl->md_dev directly to new_decode_dev() silently truncate
> 64-bit device numbers, and is the MINOR(dev) check in mdctl_get_mddev_dev()
> reachable?
>
> In struct mdu_ioctl, md_dev is __u64, whereas new_decode_dev() takes a u32.
> In 64-bit userspace, dev_t encodings (such as glibc makedev()) store
> minor[31:20] and major[31:12] in the upper 32 bits. Passing md_ctl->md_dev
> to new_decode_dev() without checking that it fits in u32 (or using
> huge_decode_dev()) discards the upper 32 bits, so a device number with
> minor = (1 << 20) decodes as minor 0 and aliases to md0 instead of failing
> with -EINVAL.
>
> In addition, in mdctl_get_mddev_dev():
>
> 	if (MINOR(dev) >= (1 << MINORBITS))
> 		return ret;
>
> MINOR(dev) is defined as ((unsigned int)((dev) & MINORMASK)) where MINORMASK
> is ((1U << MINORBITS) - 1), so MINOR(dev) >= (1 << MINORBITS) is always
> false.
>
>> +
>> +out:
>> +	mutex_unlock(&disks_mutex);
>> +	return ret;
>> +}
>
> [ ... ]
>
>> +static long mdctl_ioctl(struct file *filp, uint cmd, ulong u)
>> +{
>
> [ ... ]
>
>> +	/* These commands do not need reconfig_mutex. */
>> +	err = -ENODEV;
>> +	switch (cmd) {
>> +	case MD_CTL_GET_ARRAY_INFO:
>> +		if (mddev_configured(mddev))
>> +			err = mdctl_get_array_info(mddev, arg);
>> +		goto out;
>> +
>> +	case MD_CTL_GET_DISK_INFO:
>> +		if (mddev_configured(mddev))
>> +			err = mdctl_get_disk_info(mddev, arg);
>> +		goto out;
>> +
>> +	case MD_CTL_GET_BITMAP_FILE:
>> +		err = get_bitmap_file(mddev, arg);
>> +		goto out;
>> +
>> +	case MD_CTL_SET_DISK_FAULTY:
>> +		err = set_disk_faulty(mddev, new_decode_dev(md_ctl.arg));
>> +		goto out;
>> +	}
>
> [Severity: Critical]
> Can calling set_disk_faulty() here without reconfig_mutex or an opener
> reference race with __md_stop() and trigger a use-after-free or NULL pointer
> dereference?
>
> In the legacy block device md_ioctl() path, the caller holds /dev/mdX open,
> so mddev->openers >= 1 prevents another task from stopping the array via
> STOP_ARRAY. Through /dev/md-control, mdctl_ioctl() does not open the block
> device, so mddev->openers is 0 and MD_CTL_SET_DISK_FAULTY runs without
> reconfig_mutex.
>

Yes, The old path is not fully safe either. after fork(), both tasks
share the same open file, so one task can stop the array with STOP_ARRAY
while the other sets a disk faulty.

I will fix poth.

> If another task concurrently stops the array via MD_CTL_STOP_ARRAY (or writes
> "inactive" or "clear" to sysfs array_state), do_md_stop() calls
> __md_stop(mddev) before export_array(mddev) unbinds the rdevs:
>
> drivers/md/md.c:__md_stop() {
> 	...
> 	spin_lock(&mddev->lock);
> 	mddev->pers = NULL;
> 	spin_unlock(&mddev->lock);
> 	if (mddev->private)
> 		pers->free(mddev, mddev->private);
> 	mddev->private = NULL;
> 	put_pers(pers);
> }
>
> Concurrently, set_disk_faulty() only takes rcu_read_lock(), finds the still
> bound rdev via md_find_rdev_rcu(), and calls md_error(mddev, rdev):
>
> drivers/md/md.c:md_error() {
> 	...
> 	if (!mddev->pers || !mddev->pers->error_handler)
> 		return;
> 	mddev->pers->error_handler(mddev, rdev);
>
> 	if (mddev->pers->head.id == ID_RAID0 ||
> 	    mddev->pers->head.id == ID_LINEAR)
> 		return;
> 	...
> }
>
> Because __md_stop() clears mddev->pers and immediately frees mddev->private
> (such as struct r1conf, r10conf, or r5conf) without waiting for an RCU grace
> period or holding reconfig_mutex against set_disk_faulty(), md_error() and
> mddev->pers->error_handler() can dereference a NULL mddev->pers or a freed
> mddev->private.
>
>> +
>> +	if (cmd == MD_CTL_STOP_ARRAY) {
>> +		/* Need to flush page cache, and ensure no openers */
>> +		err = mddev_set_closing_and_sync_blockdev(mddev, 0);
>> +		if (err)
>> +			goto out;
>> +
>> +		closing = true;
>> +	}
>> +
>> +	if (!md_is_rdwr(mddev))
>> +		flush_work(&mddev->sync_work);
>> +
>> +	suspend = mdctl_ioctl_need_suspend(cmd);
>> +	err = suspend ? mddev_suspend_and_lock(mddev) : mddev_lock(mddev);
>> +	if (err) {
>> +		pr_debug("md: ioctl lock interrupted, reason %d, cmd %d\n",
>> +			 err, cmd);
>> +		goto out;
>> +	}
>
> [Severity: High]
> Does mdctl_ioctl() or mdctl_find_mddev() need to check whether MD_CLOSING is
> set on mddev->flags?
>
> When legacy_async_del_gendisk is true (the default), stopping an array where
> mddev->hold_active == 0 via do_md_stop(mddev, 0) -> md_clean() leaves
> MD_CLOSING set on mddev->flags without setting MD_DELETED:
>
> drivers/md/md.c:md_clean() {
> 	...
> 	if (legacy_async_del_gendisk && mddev->hold_active) {
> 		clear_bit(MD_CLOSING, &mddev->flags);
> 	} else {
> 		/* if UNTIL_STOP is set, it's cleared here */
> 		mddev->hold_active = 0;
> 		/* Don't clear MD_CLOSING, or mddev can be opened again. */
> 		mddev->flags &= BIT_ULL_MASK(MD_CLOSING);
> 	}
> }
>
> For legacy block device ioctls, md_open() checks test_bit(MD_CLOSING,
> &mddev->flags) and returns -ENODEV, preventing any new ioctls from
> reconfiguring the array before mddev_put() -> __mddev_put() sets MD_DELETED
> and queues del_work.
>
> However, mdctl_find_mddev() -> mddev_get() and mddev_lock() only check
> MD_DELETED, not MD_CLOSING. If a concurrent task issues MD_CTL_SET_ARRAY_INFO
> or MD_CTL_ADD_NEW_DISK via /dev/md-control and grabs a reference in
> mdctl_find_mddev() before the stopping task's mddev_put() drops
> mddev->active to 0, __mddev_put() will not run when the stop finishes. The
> second task then locks the mddev and populates mddev->ctime,
> mddev->raid_disks, or mddev->disks while MD_CLOSING is still set. When
> mddev_put() later drops mddev->active to 0, __mddev_put() sees that the
> mddev is no longer empty and returns early without deleting it:
>
> drivers/md/md.c:__mddev_put() {
> 	if (mddev->raid_disks || !list_empty(&mddev->disks) ||
> 	    mddev->ctime || mddev->hold_active)
> 		return;
> 	...
> }
>
> This leaves MD_CLOSING permanently set on the mddev, leaking the mddev and
> its bound rdevs and causing subsequent md_open() calls to fail with -ENODEV
> and MD_CTL_STOP_ARRAY calls to fail with -EBUSY.
>

Yes, I will fix this in the next version.

>> +	if (suspend)
>> +		noio_flags = memalloc_noio_save();
>> +
>> +	/*
>> +	 * Commands querying/configuring an existing array.
>> +	 *
>> +	 * If the array is not initialised yet, check of the command requires
>> +	 * the array to be initialised.
>> +	 */
>> +	if (!mddev_configured(mddev) && mdctl_ioctl_need_config(cmd, flags)) {
>> +		err = -ENODEV;
>> +		goto unlock;
>> +	}
>> +
>> +	/*
>> +	 * Commands even a read-only array can execute:
>> +	 */
>> +	switch (cmd) {
>> +	case MD_CTL_SET_ARRAY_INFO:
>> +		err = mdctl_set_array_info(mddev, arg);
>> +		goto unlock;
>> +
>> +	case MD_CTL_RESTART_ARRAY_RW:
>> +		err = restart_array(mddev);
>> +		goto unlock;
>> +
>> +	case MD_CTL_STOP_ARRAY:
>> +		err = -EINVAL;
>> +		if (!flags)
>> +			err = do_md_stop(mddev, 0);
>> +		else if (flags == MD_CTL_RO_FLAG && mddev->pers)
>> +			err = md_set_readonly(mddev);
>> +		goto unlock;
>
> [Severity: Medium]
> What happens when MD_CTL_STOP_ARRAY is called with flags == MD_CTL_RO_FLAG
> on a configured array that is not currently running (mddev->pers == NULL)?
>
> In the legacy md_ioctl() handler, err is initialized to 0 before the switch:
>
> drivers/md/md.c:md_ioctl() {
> 	...
> 	case STOP_ARRAY_RO:
> 		if (mddev->pers)
> 			err = md_set_readonly(mddev);
> 		goto unlock;
> }
>
> so STOP_ARRAY_RO on a configured but inactive array succeeds with 0 (and
> clears mddev->hold_active == UNTIL_IOCTL at unlock).
>
> In mdctl_ioctl(), err is set to -EINVAL before the if/else chain, so when
> flags == MD_CTL_RO_FLAG and mddev->pers is NULL, neither branch is taken and
> mdctl_ioctl() returns -EINVAL.
>

This is intentional. STOP_ARRAY_RO returns 0 for an array that is not
running, although nothing was switched to read-only.

>> +
>> +	case MD_CTL_HOT_REMOVE_DISK:
>> +		err = hot_remove_disk(mddev, new_decode_dev(md_ctl.arg));
>> +		goto unlock;
>> +
>> +	case MD_CTL_ADD_NEW_DISK:
>> +		/* We can support ADD_NEW_DISK on read-only arrays
>> +		 * only if we are re-adding a preexisting device.
>> +		 * So require mddev->pers and MD_DISK_SYNC.
>> +		 */
>> +		if (mddev->pers) {
>> +			struct mdu_disk_info64 info;
>> +
>> +			if (copy_from_user(&info, arg, sizeof(info)))
>> +				err = -EFAULT;
>> +			else if (!(info.state & (1 << MD_DISK_SYNC)))
>> +				/* Need to clear read-only for this */
>> +				break;
>> +			else
>> +				err = md_add_new_disk(mddev, &info);
>> +			goto unlock;
>> +		}
>> +		break;
>> +	}
>> +
>> +	/*
>> +	 * The remaining ioctls are changing the state of the
>> +	 * superblock, so we do not allow them on read-only arrays.
>> +	 */
>> +	if (!md_is_rdwr(mddev) && mddev->pers) {
>> +		if (mddev->ro != MD_AUTO_READ) {
>> +			err = -EROFS;
>> +			goto unlock;
>> +		}
>> +		mddev->ro = MD_RDWR;
>> +		sysfs_notify_dirent_safe(mddev->sysfs_state);
>> +		set_bit(MD_RECOVERY_NEEDED, &mddev->recovery);
>> +		/* mddev_unlock will wake thread */
>> +		/* If a device failed while we were read-only, we
>> +		 * need to make sure the metadata is updated now.
>> +		 */
>> +		if (test_bit(MD_SB_CHANGE_DEVS, &mddev->sb_flags)) {
>> +			mddev_unlock(mddev);
>> +			wait_event(mddev->sb_wait,
>> +				   !test_bit(MD_SB_CHANGE_DEVS, &mddev->sb_flags) &&
>> +				   !test_bit(MD_SB_CHANGE_PENDING, &mddev->sb_flags));
>> +			mddev_lock_nointr(mddev);
>> +		}
>> +	}
>
> [Severity: High]
> Can the array be stopped and deleted while mdctl_ioctl() drops
> reconfig_mutex to wait on mddev->sb_wait here?
>
> In md_ioctl(), the caller holds the block device open (mddev->openers >= 1),
> which prevents a concurrent STOP_ARRAY from passing
> mddev_set_closing_and_sync_blockdev() while reconfig_mutex is dropped.
> In mdctl_ioctl(), the block device is not open (mddev->openers == 0).
>
> When mdctl_ioctl() drops reconfig_mutex via mddev_unlock(mddev) and sleeps
> in wait_event(mddev->sb_wait, ...), a concurrent MD_CTL_STOP_ARRAY (which
> does not call mddev_suspend()) or a sysfs array_state write can acquire
> reconfig_mutex and run do_md_stop(mddev, 0). That stops the personality,
> exports all disks, clears mddev->sb_flags and mddev->raid_disks in
> md_clean(), sets MD_DELETED (when !legacy_async_del_gendisk), and in
> mddev_unlock() wakes mddev->sb_wait and deletes the gendisk.
>
> mdctl_ioctl() then wakes up, reacquires reconfig_mutex via
> mddev_lock_nointr(mddev) (which does not check MD_DELETED or MD_CLOSING),
> and proceeds into the switch below without revalidating that the array is
> still active or configured. For example, if cmd is MD_CTL_ADD_NEW_DISK,
> md_add_new_disk() will see mddev->raid_disks == 0 and bind a new rdev onto
> the stopped/deleted mddev, preventing __mddev_put() from freeing the mddev
> and leaving the rdev's block device claimed.
>
> -- 
> Sashiko AI review · https://sashiko.dev/#/patchset/20261029202209.1422567-1-abd.masalkhi@gmail.com?part=4

-- 
Best Regards,
Abd-Alrhman

      reply	other threads:[~2026-10-30  8:35 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-29 20:22 [RFC PATCH v5 0/4] md: add a control device for array management Abd-Alrhman Masalkhi
2026-10-29 20:22 ` [RFC PATCH v5 1/4] md: add uapi definitions for the md control device Abd-Alrhman Masalkhi
2026-10-29 20:22 ` [RFC PATCH v5 2/4] md: pass struct mdu_disk_info64 to md_add_new_disk() Abd-Alrhman Masalkhi
2026-10-29 20:22 ` [RFC PATCH v5 3/4] md: use struct mdu_array_info64 for SET_ARRAY_INFO Abd-Alrhman Masalkhi
2026-10-29 20:22 ` [RFC PATCH v5 4/4] md: add a control device for array management Abd-Alrhman Masalkhi
2026-10-29 20:48   ` sashiko-bot
2026-10-30  8:35     ` Abd-Alrhman Masalkhi [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=m2wlqz1t7b.fsf@gmail.com \
    --to=abd.masalkhi@gmail.com \
    --cc=linux-raid@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=yukuai@fygo.io \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox