Re: [PATCH] md: avoid block autoload while importing rdev under reconfig_mutex

From: 王继晨

Date: Wed Nov 11 2026 - 01:37:52 EST



> From: "yu kuai"<yukuai@xxxxxxx>
> Date:  Sun, Nov 8, 2026, 11:18
> Subject:  Re: [PATCH] md: avoid block autoload while importing rdev under reconfig_mutex
> To: "Wang Jichen"<wangjichen@xxxxxxxxxxx>, "Jens Axboe"<axboe@xxxxxxxxx>, "Song Liu"<song@xxxxxxxxxx>, "Li Nan"<magiclinan@xxxxxxxxxxxxxx>, "Xiao Ni"<xiao@xxxxxxxxxx>, <linux-block@xxxxxxxxxxxxxxx>, <linux-kernel@xxxxxxxxxxxxxxx>, <linux-raid@xxxxxxxxxxxxxxx>, "yu kuai"<yukuai@xxxxxxx>
> Cc: <syzbot+cd254889291ef8666036@xxxxxxxxxxxxxxxxxxxxxxxxx>
> Hi,
> 
> 在 2026/11/3 19:55, Wang Jichen 写道:
> > From: wangjichen <wangjichen@xxxxxxxxxxx>
> >
> > md_import_device() runs with mddev->reconfig_mutex held from md_ioctl().
> > Opening a member disk via bdev_file_open_by_dev() triggers legacy block
> > autoload (blk_probe_dev), which takes major_names_lock. md_probe() can
> > run under that lock and call md_alloc(), which takes disks_mutex and
> > reconfig_mutex — a lockdep ABBA with the ioctl path.
> 
> Is this a real deadlock? mddev is already existed from md_import_device(),
> while md_alloc() just allocate a new mddev, so the reconfig_mtuex is not
> the same.

Hi Kuai,

Thanks for the question.

You're right that md_ioctl() holds reconfig_mutex on the existing array
mddev, while md_alloc() takes reconfig_mutex on a newly allocated mddev —
they are different per-device mutex instances, not the same lock.

The syzbot/lockdep report is about a lock *class* cycle, not a
single-mddev self-deadlock. Lockdep treats all &mddev->reconfig_mutex as
one class, so the dependency it reports is:

  reconfig_mutex -> major_names_lock -> disks_mutex -> reconfig_mutex

Concrete race (syzbot extid cd254889291ef8666036, C repro on QEMU):

- Thread doing ADD_NEW_DISK on array A holds A's reconfig_mutex, then
  md_import_device() opens a member via bdev_file_open_by_dev(), which
  with legacy autoload calls blk_probe_dev() and takes major_names_lock.

- Concurrent open of an unregistered md node takes major_names_lock in
  blk_probe_dev(), runs md_probe() -> md_alloc() for device B, takes
  disks_mutex, then B's reconfig_mutex.

Without lockdep, the same ABBA ordering between threads can still hang;
the syzbot report includes the standard CPU0/CPU1 "Possible unsafe
locking scenario" text for this chain.

For context on when reconfig_mutex entered the md_alloc() path: the cycle
became visible after the fix for the half-initialised kobj window:

  ca39f7502425 ("md: fix mddev->kobj lifetime")
  d1472ca2ca5d ("md: don't hand out the array before md_alloc() has added
                mddev->kobj")

The latter holds reconfig_mutex across add_disk() and kobject_add() so
ioctls wait instead of touching an uninitialised mddev->kobj (Fixes:
ca39f7502425). That closes the kobject race but completes this lockdep
ring with ioctl + legacy autoload on the other side.

This patch does not revert that behaviour. It only breaks the ioctl-side
edge by not triggering legacy block autoload while reconfig_mutex is
held: md_import_device() uses bdev_file_open_by_dev_autoload(..., false).
Member disks are expected to already exist for normal mdadmin usage; the
syz repro still gets -ENXIO for bogus minors, but on a fixed kernel there
is no lockdep warning (Tested with syzbot's C repro on QEMU, 6628e7d4
bzImage, before/after).

If maintainers prefer a block-layer approach instead (e.g. not calling
md probe callbacks under major_names_lock in blk_probe_dev()), I'm happy
to respin.

Thanks,
Jichen

> 
> >
> > Add bdev_file_open_by_dev_autoload() and use autoload=false from
> > md_import_device(); member devices must already be present in the
> > block layer (normal mdadmin usage).
> >
> > Link: https://syzbot.org/bug?extid=cd254889291ef8666036
> > Reported-by: syzbot+cd254889291ef8666036@xxxxxxxxxxxxxxxxxxxxxxxxx
> > Tested-by: Wang Jichen <wangjichen@xxxxxxxxxxx>
> > Signed-off-by: Wang Jichen <wangjichen@xxxxxxxxxxx>
> > ---
> >   block/bdev.c           | 13 ++++++++++---
> >   drivers/md/md.c        |  8 ++++++--
> >   include/linux/blkdev.h |  2 ++
> >   3 files changed, 18 insertions(+), 5 deletions(-)
> >
> > diff --git a/block/bdev.c b/block/bdev.c
> > index f20f91b31..e78936556 100644
> > --- a/block/bdev.c
> > +++ b/block/bdev.c
> > @@ -1076,8 +1076,8 @@ static unsigned blk_to_file_flags(blk_mode_t mode)
> >           return flags;
> >   }
> >   
> > -struct file *bdev_file_open_by_dev(dev_t dev, blk_mode_t mode, void *holder,
> > -                                   const struct blk_holder_ops *hops)
> > +struct file *bdev_file_open_by_dev_autoload(dev_t dev, blk_mode_t mode,
> > +                void *holder, const struct blk_holder_ops *hops, bool autoload)
> >   {
> >           struct file *bdev_file;
> >           struct block_device *bdev;
> > @@ -1088,7 +1088,7 @@ struct file *bdev_file_open_by_dev(dev_t dev, blk_mode_t mode, void *holder,
> >           if (ret)
> >                   return ERR_PTR(ret);
> >   
> > -        bdev = blkdev_get_no_open(dev, true);
> > +        bdev = blkdev_get_no_open(dev, autoload);
> >           if (!bdev)
> >                   return ERR_PTR(-ENXIO);
> >   
> > @@ -1110,6 +1110,13 @@ struct file *bdev_file_open_by_dev(dev_t dev, blk_mode_t mode, void *holder,
> >           }
> >           return bdev_file;
> >   }
> > +EXPORT_SYMBOL(bdev_file_open_by_dev_autoload);
> > +
> > +struct file *bdev_file_open_by_dev(dev_t dev, blk_mode_t mode, void *holder,
> > +                                   const struct blk_holder_ops *hops)
> > +{
> > +        return bdev_file_open_by_dev_autoload(dev, mode, holder, hops, true);
> > +}
> >   EXPORT_SYMBOL(bdev_file_open_by_dev);
> >   
> >   struct file *bdev_file_open_by_path(const char *path, blk_mode_t mode,
> > diff --git a/drivers/md/md.c b/drivers/md/md.c
> > index 03e35386a..d3717e2e8 100644
> > --- a/drivers/md/md.c
> > +++ b/drivers/md/md.c
> > @@ -3858,9 +3858,13 @@ static struct md_rdev *md_import_device(dev_t newdev, int super_format, int supe
> >           if (err)
> >                   goto out_clear_rdev;
> >   
> > -        rdev->bdev_file = bdev_file_open_by_dev(newdev,
> > +        /*
> > +         * md_ioctl() holds reconfig_mutex; legacy autoload in bdev open takes
> > +         * major_names_lock via blk_probe_dev() and can deadlock with md_probe().
> > +         */
> > +        rdev->bdev_file = bdev_file_open_by_dev_autoload(newdev,
> >                           BLK_OPEN_READ | BLK_OPEN_WRITE,
> > -                        super_format == -2 ? &claim_rdev : rdev, NULL);
> > +                        super_format == -2 ? &claim_rdev : rdev, NULL, false);
> >           if (IS_ERR(rdev->bdev_file)) {
> >                   pr_warn("md: could not open device unknown-block(%u,%u).\n",
> >                           MAJOR(newdev), MINOR(newdev));
> > diff --git a/include/linux/blkdev.h b/include/linux/blkdev.h
> > index 0c2010230..e5121101a 100644
> > --- a/include/linux/blkdev.h
> > +++ b/include/linux/blkdev.h
> > @@ -1699,6 +1699,8 @@ struct blk_holder_ops {
> >   
> >   struct file *bdev_file_open_by_dev(dev_t dev, blk_mode_t mode, void *holder,
> >                   const struct blk_holder_ops *hops);
> > +struct file *bdev_file_open_by_dev_autoload(dev_t dev, blk_mode_t mode,
> > +                void *holder, const struct blk_holder_ops *hops, bool autoload);
> >   struct file *bdev_file_open_by_path(const char *path, blk_mode_t mode,
> >                   void *holder, const struct blk_holder_ops *hops);
> >   int bd_prepare_to_claim(struct block_device *bdev, void *holder,
> 
> -- 
> Thanks,
> Kuai
>