Bug Analysis: Circular Locking Dependency in mtdblock / mtdconcat Release Path
1. Mechanism and Involved Files & Entities
- Files:
- Structs and Fields:
struct mtd_blktrans_dev (dev->lock)
struct mtdblk_dev (mtdblk->cache_mutex, mtdblk->cache_state, mtdblk->cache_data)
struct erase_info
2. Root Cause Analysis
Lockdep detected a circular locking dependency between memory reclaim (fs_reclaim) and the MTD block translation locks (&dev->lock and &mtdblk->cache_mutex):
fs_reclaim --> &dev->lock --> &mtdblk->cache_mutex --> fs_reclaim
The cycle arises from the interaction between two paths:
A. Block I/O path: fs_reclaim -> ... -> &dev->lock -> &mtdblk->cache_mutex
- During memory pressure, kernel memory allocation enters direct reclaim (
fs_reclaim).
- Reclaim may attempt to flush dirty pages or issue I/O to block devices, entering the block request dispatch infrastructure (
q->q_usage_counter(io) -> q->elevator_lock -> set->srcu -> blk_mq_dispatch_rq_list -> mtd_queue_rq()).
- In
drivers/mtd/mtd_blkdevs.c, mtd_queue_rq() calls mtd_blktrans_work(), which acquires mutex_lock(&dev->lock) (line 152) to service each request via do_blktrans_request().
- If the request is a flush (
REQ_OP_FLUSH), do_blktrans_request() calls tr->flush(dev) (mtdblock_flush() in drivers/mtd/mtdblock.c), which acquires mutex_lock(&mtdblk->cache_mutex).
- Although
mtd_queue_rq() wraps request processing in memalloc_noio_save() / memalloc_noio_restore() to prevent reclaim recursion during normal request handling, this only protects allocations made inside mtd_queue_rq().
B. Device release path: &dev->lock -> &mtdblk->cache_mutex -> fs_reclaim
- When closing an MTD block device (
close() -> bdev_release() -> blktrans_release() in drivers/mtd/mtd_blkdevs.c), mutex_lock(&dev->lock) is acquired at line 237 without memalloc_noio_save().
blktrans_release() then invokes dev->tr->release(dev), which calls mtdblock_release() (drivers/mtd/mtdblock.c:282).
mtdblock_release() acquires mutex_lock(&mtdblk->cache_mutex) (line 288) and calls write_cached_data() to flush any dirty cached flash sector before closing.
write_cached_data() calls erase_write() -> mtd_erase() -> master->_erase().
- When the underlying MTD device is a concatenated device (
mtdconcat), _erase resolves to concat_erase() in drivers/mtd/mtdconcat.c.
- At line 407 in
drivers/mtd/mtdconcat.c:
/* make a local copy of instr to avoid modifying the caller's struct */
erase = kmalloc_obj(struct erase_info);
kmalloc_obj(struct erase_info) defaults to GFP_KERNEL allocation flags. This allocates memory capable of sleeping and triggering direct memory reclaim (fs_reclaim) while holding dev->lock and mtdblk->cache_mutex.
- This dynamic allocation is completely unnecessary:
struct erase_info is only 24 bytes (three 64-bit integers) and is used strictly synchronously within concat_erase() (and freed at line 464). Other functions like mtd_erase() and erase_write() allocate struct erase_info directly on the stack.
3. Failure Scenario
If kmalloc_obj(struct erase_info) in concat_erase() encounters memory pressure while /dev/mtdblock* is being closed, the task enters fs_reclaim while holding both dev->lock and mtdblk->cache_mutex. If direct reclaim initiates I/O or writeback on the same block device, the reclaim path will wait on mutex_lock(&dev->lock) in mtd_blktrans_work(), leading to an unrecoverable ABBA deadlock.