Root Cause Analysis
1. Mechanism and Crash Site
The warning is triggered in block/holder.c:147 inside bd_unlink_disk_holder():
void bd_unlink_disk_holder(struct block_device *bdev, struct gendisk *disk)
{
struct bd_holder_disk *holder;
...
mutex_lock(&blk_holder_mutex);
holder = bd_find_holder_disk(bdev, disk);
if (!WARN_ON_ONCE(holder == NULL) && !--holder->refcnt) {
...
bd_unlink_disk_holder() expects that every unlinking operation corresponds to a previously established holder relationship recorded in disk->slave_bdevs. When bd_find_holder_disk() traverses disk->slave_bdevs and cannot find an entry whose holder_dir matches bdev->bd_holder_dir, it returns NULL, triggering WARN_ON_ONCE(holder == NULL).
The call trace demonstrates this occurring when stopping an MD array (STOP_ARRAY ioctl):
do_md_stop() -> export_array() -> md_kick_rdev_from_array() -> bd_unlink_disk_holder()
For each struct md_rdev *rdev remaining in mddev->disks, export_array() calls md_kick_rdev_from_array(rdev), which in turn calls bd_unlink_disk_holder(rdev->bdev, rdev->mddev->gendisk).
In the MD subsystem, an rdev is added to mddev->disks exclusively by bind_rdev_to_array():
static int bind_rdev_to_array(struct md_rdev *rdev, struct mddev *mddev)
{
...
list_add_rcu(&rdev->same_set, &mddev->disks);
bd_link_disk_holder(rdev->bdev, mddev->gendisk);
return 0;
...
}
There are two key flaws in this implementation:
- Unchecked Return Value of
bd_link_disk_holder():
bind_rdev_to_array() calls bd_link_disk_holder() without checking its return value and returns 0 (success). If bd_link_disk_holder() fails, the rdev remains attached to mddev->disks, but no struct bd_holder_disk is allocated or linked to mddev->gendisk->slave_bdevs.
- Missing Invariant Check on Self-Referencing Disks:
bind_rdev_to_array() does not check whether rdev->bdev->bd_disk == mddev->gendisk. Inside bd_link_disk_holder() (block/holder.c:70):
if (bdev->bd_disk == disk)
return -EINVAL;
A block device cannot hold itself or its own partitions.
3. How the Bug is Triggered
- An MD array (e.g.,
/dev/md4) is started with non-zero capacity (as indicated by the log: md4: detected capacity change from 4096 to 0).
- An ioctl such as
ADD_NEW_DISK or HOT_ADD_DISK is executed, passing the device number (dev_t) of /dev/md4 itself (or one of its partitions).
md_import_device() opens the device via bdev_file_open_by_dev(). Since the device is active and has non-zero size, it successfully creates the struct md_rdev.
bind_rdev_to_array() is called. It checks for duplicate dev_t entries among existing rdevs via find_rdev(), which passes if /dev/md4 was not previously in the array.
bind_rdev_to_array() places rdev onto &mddev->disks via list_add_rcu(), and calls bd_link_disk_holder(rdev->bdev, mddev->gendisk).
- Because
rdev->bdev->bd_disk == mddev->gendisk, bd_link_disk_holder() returns -EINVAL without allocating or linking a bd_holder_disk. (Similarly, this occurs if bd_link_disk_holder() fails due to -ENODEV, -ENOMEM, or sysfs link creation failure).
bind_rdev_to_array() ignores the -EINVAL return code and returns 0.
- Later, userspace stops the array via
STOP_ARRAY (do_md_stop(mddev, 0)). export_array() iterates over mddev->disks and invokes md_kick_rdev_from_array(), which calls bd_unlink_disk_holder(rdev->bdev, mddev->gendisk).
bd_find_holder_disk() returns NULL, triggering the warning at block/holder.c:147.