[PATCH v3 2/2] mtd: spi-nor: fix use-after-free on unbind with open handles

NG, TZE YEE tze.yee.ng at altera.com
Mon Nov 2 19:53:43 PST 2026


Hi,

Gentle ping on this patch. Any chance for a review when you have time?

Thanks,
Tze Yee


On 23/10/2026 10:03 am, NG, TZE YEE wrote:
> 
> Hi maintainers,
> 
> Thanks for the review. These four are real, but they are not new in this
> series, and I'd prefer to keep it limited to the unbind use-after-free
> on the I/O paths:
> 
> 
> On 22/10/2026 12:18 pm, sashiko-bot at kernel.org wrote:
>> Thank you for your contribution! Sashiko AI review found 4 potential 
>> issue(s) to consider:
>> - [Critical] mtd: spi-nor: use-after-free in spi_nor_suspend() and 
>> spi_nor_resume()
>> - [High] mtd: spi-nor: use-after-free of nor->dev in dev_dbg() after 
>> unregistration
>> - [High] mtd: spi-nor: use-after-free of devres-backed nor->mtd.name 
>> after unbind
>> - [High] mtd: spi-nor: missing nor->removed check in 
>> spi_nor_get_device() after unbind
>> -- 
>>
>> commit de3094832a517827e88d8d0203973cdc91471e95
>> Author: Tze Yee Ng <tze.yee.ng at altera.com>
>>
>> mtd: spi-nor: fix use-after-free on unbind with open handles
>>
>> This commit moves spi_nor and its associated buffers off devres on the
>> spi-mem probe path and refcounts struct spi_nor across registration 
>> and open
>> MTD handles. During remove(), it unregisters debugfs and the MTD 
>> device, marks
>> nor->removed under the lock to fence subsequent operations with - 
>> ENODEV, and
>> drops the registration reference.
>>
>>> diff --git a/drivers/mtd/spi-nor/core.c b/drivers/mtd/spi-nor/core.c
>>> index 8bc117b46e028..76928bd8413ed 100644
>>> --- a/drivers/mtd/spi-nor/core.c
>>> +++ b/drivers/mtd/spi-nor/core.c
>>
>> [ ... ]
>>
>>> @@ -3420,20 +3449,48 @@ static void spi_nor_resume(struct mtd_info *mtd)
>>>           dev_err(dev, "resume() failed\n");
>>>   }
>>
>> [Severity: Critical]
>> What happens if the system suspends and resumes after spi_nor_remove() 
>> while
>> an open file descriptor still holds a reference on the MTD device?
>>
>> When a partition (or a master device with CONFIG_MTD_PARTITIONED_MASTER
>> enabled) is open during spi_nor_remove(), del_mtd_device() drops mtd- 
>> >refcnt
>> from 2 to 1 without running mtd_device_release() -> 
>> device_unregister(), so
>> mtd->dev remains registered on mtd_class with mtd_cls_pm_ops active:
>>
>>    mtd_cls_suspend() -> mtd_suspend() -> spi_nor_suspend()
>>    mtd_cls_resume()  -> mtd_resume()  -> spi_nor_resume()
>>
>> Meanwhile, once spi_nor_remove() returns, devres_release_all(&spi- 
>> >dev) frees
>> nor->spimem, nor->dirmap, nor->params->vreg_offset, and nor->params- 
>> >priv, and
>> disables the vcc regulator:
>>
>>    drivers/mtd/spi-nor/core.c:spi_nor_suspend() {
>>        struct spi_nor *nor = mtd_to_spi_nor(mtd);
>>        int ret;
>>
>>        /* Disable octal DTR mode if we enabled it. */
>>        ret = spi_nor_set_octal_dtr(nor, false);
>>        ...
>>    }
>>
>>    drivers/mtd/spi-nor/core.c:spi_nor_resume() {
>>        struct spi_nor *nor = mtd_to_spi_nor(mtd);
>>        struct device *dev = nor->dev;
>>        int ret;
>>
>>        /* re-initialize the nor chip */
>>        ret = spi_nor_init(nor);
>>        ...
>>    }
>>
>> Since neither spi_nor_suspend() nor spi_nor_resume() acquires nor- 
>> >lock or
>> checks nor->removed, won't a suspend/resume cycle execute SPI operations
>> through the freed nor->spimem and freed params fields?
>>
> 
> Agreed. Mainline already runs these on a freed spi_nor after unbind;
> with this series the spi_nor stays allocated until the last close, so it
> is narrower, but the suspend/resume paths still reach freed bus
> resources. Needs the nor lock and a removed check in both. Left for a
> follow-up.
> 
>>> -static int spi_nor_get_device(struct mtd_info *mtd)
>>> +/* Free spi_nor and its buffers; safe on partial init. */
>>> +static void spi_nor_free(struct spi_nor *nor)
>>> +{
>>> +    spi_nor_debugfs_unregister(nor);
>>> +    kfree(nor->mtd.eraseregions);
>>> +    kfree(nor->bouncebuf);
>>> +    kfree(nor->params);
>>> +    kfree(nor);
>>> +}
>>> +
>>> +static void spi_nor_release(struct kref *kref)
>>> +{
>>> +    spi_nor_free(container_of(kref, struct spi_nor, refcnt));
>>> +}
>>> +
>>> +static struct module *spi_nor_controller_owner(struct spi_nor *nor)
>>>   {
>>> -    struct mtd_info *master = mtd_get_master(mtd);
>>> -    struct spi_nor *nor = mtd_to_spi_nor(master);
>>>       struct device *dev;
>>> +    /* The controller may be gone on the last put; use the cached 
>>> owner. */
>>> +    if (nor->refcounted)
>>> +        return nor->controller_module;
>>> +
>>>       if (nor->spimem)
>>>           dev = nor->spimem->spi->controller->dev.parent;
>>>       else
>>>           dev = nor->dev;
>>> -    if (!try_module_get(dev->driver->owner))
>>> +    return dev->driver->owner;
>>> +}
>>> +
>>> +static int spi_nor_get_device(struct mtd_info *mtd)
>>> +{
>>> +    struct mtd_info *master = mtd_get_master(mtd);
>>> +    struct spi_nor *nor = mtd_to_spi_nor(master);
>>> +
>>> +    if (!try_module_get(spi_nor_controller_owner(nor)))
>>>           return -ENODEV;
>>> +    if (nor->refcounted)
>>> +        kref_get(&nor->refcnt);
>>> +
>>>       return 0;
>>>   }
>>
>> [Severity: High]
>> Should spi_nor_get_device() fail with -ENODEV once nor->removed is set?
>>
>> When spi_nor_remove() runs while a handle is open on a partition (or 
>> master
>> with CONFIG_MTD_PARTITIONED_MASTER), mtd_device_unregister() only 
>> decrements
>> mtd->refcnt without calling mtd_device_release(), so the MTD device 
>> stays in
>> mtd_idr until the open handle is closed and mtd_release() runs:
>>
>>    mtdchar_open()
>>      -> get_mtd_device()
>>        -> __get_mtd_device()
>>          -> spi_nor_get_device()
>>
>> Without checking nor->removed (or using kref_get_unless_zero()), new 
>> opens of
>> /dev/mtdX after unbind will still succeed and take new references on
>> nor->refcnt, allowing userspace to keep struct spi_nor and the controller
>> module pinned indefinitely after the original pre-unbind handle is 
>> closed.
>>
>> In addition, in __put_mtd_device(), master->_put_device(master) ->
>> spi_nor_put_device() frees struct spi_nor immediately when the last 
>> handle
>> closes, whereas idr_remove(&mtd_idr, mtd->index) only runs in 
>> mtd_release()
>> when all device references on mtd->dev drop. If anything holds a 
>> temporary
>> reference on mtd->dev across the final close, could a concurrent
>> get_mtd_device() still find the mtd in mtd_idr and call 
>> spi_nor_get_device()
>> on a freed struct spi_nor?
>>
>> [ ... ]
>>
> 
> Agreed. A new open after unbind succeeds and pins the object, and its
> I/O then fails with -ENODEV. A removed check in ->_get_device() would
> stop that; the lingering idr entry in the second paragraph is a core
> lifetime question and would need a core change. Left for a follow-up.
> 
>>> @@ -3788,12 +3870,15 @@ static int spi_nor_probe(struct spi_mem *spimem)
>>>       if (ret)
>>>           return ret;
>>> -    nor = devm_kzalloc(dev, sizeof(*nor), GFP_KERNEL);
>>> +    nor = kzalloc_obj(*nor, GFP_KERNEL);
>>>       if (!nor)
>>>           return -ENOMEM;
>>>       nor->spimem = spimem;
>>>       nor->dev = dev;
>>> +    nor->controller_module = spi->controller->dev.parent->driver- 
>>> >owner;
>>> +    nor->refcounted = true;
>>> +    kref_init(&nor->refcnt);
>>
>> [Severity: High]
>> Does nor->dev need a get_device() reference tied to nor->refcnt, or 
>> need to be
>> avoided before nor->removed is checked?
>>
>> With CONFIG_MTD_PARTITIONED_MASTER disabled, opening an unpartitioned 
>> master
>> /dev/mtd0 calls __get_mtd_device(), which increments nor->refcnt via
>> spi_nor_get_device() but does not increment master->refcnt:
>>
>>    drivers/mtd/mtdcore.c:__get_mtd_device() {
>>        ...
>>        while (mtd) {
>>            if (mtd != master)
>>                kref_get(&mtd->refcnt);
>>            mtd = mtd->parent;
>>        }
>>
>>        if (IS_ENABLED(CONFIG_MTD_PARTITIONED_MASTER))
>>            kref_get(&master->refcnt);
>>        ...
>>    }
>>
>> If the SPI controller is unbound or the SPI device is unregistered while
>> /dev/mtd0 is open, spi_nor_remove() -> mtd_device_unregister() drops
>> nor->mtd.refcnt to 0 and unregisters nor->mtd.dev immediately, 
>> releasing its
>> parent reference on &spi->dev. Once spi_unregister_device() finishes, 
>> struct
>> spi_device (nor->dev) is freed while struct spi_nor remains alive for 
>> the open
>> descriptor.
>>
>> A subsequent read, write, or erase on that open descriptor calls 
>> dev_dbg() on
>> nor->dev before spi_nor_prep_and_lock*() checks nor->removed:
>>
>>    drivers/mtd/spi-nor/core.c:spi_nor_read() {
>>        ...
>>        dev_dbg(nor->dev, "from 0x%08x, len %zd\n", (u32)from, len);
>>
>>        ret = spi_nor_prep_and_lock_rd(nor, from_lock, len_lock);
>>        ...
>>    }
>>
>> The same pattern appears in spi_nor_write(), spi_nor_erase(), and
>> sst_nor_write(). Could this dereference a freed nor->dev when debug 
>> logging is
>> enabled?
>>
> 
> Agreed for unpartitioned flash without CONFIG_MTD_PARTITIONED_MASTER.
> Holding a reference on nor->dev for the lifetime of spi_nor would fix
> it. Left for a follow-up.
> 
>>>       spi_nor_set_flash_node(nor, dev->of_node);
>>>       spi_mem_set_drvdata(spimem, nor);
>>
>> [Severity: High]
>> Right below this in spi_nor_probe(), nor->mtd.name is assigned from
>> spi_mem_get_name(spimem):
>>
>>    drivers/mtd/spi-nor/core.c:spi_nor_probe() {
>>        ...
>>        if (data && data->name)
>>            nor->mtd.name = data->name;
>>
>>        if (!nor->mtd.name)
>>            nor->mtd.name = spi_mem_get_name(spimem);
>>        ...
>>    }
>>
>> In spi_mem_probe(), spimem->name is either dev_name(&spi->dev) or a 
>> string
>> allocated via ctlr->mem_ops->get_name(mem), which drivers such as
>> spi-nxp-fspi, spi-fsl-qspi, spi-nxp-xspi, spi-aspeed-smc, and
>> spi-cadence-quadspi allocate with devm_kasprintf().
>>
>> When spi-nor or the SPI controller is unbound while an open handle keeps
>> nor->mtd alive in mtd_idr and sysfs, devres_release_all() frees that name
>> buffer. Won't subsequent reads of /proc/mtd (mtd_proc_show()),
>> /sys/class/mtd/mtdX/name (mtd_name_show()), or lookups via
>> get_mtd_device_nm() read freed memory from nor->mtd.name unless the 
>> string is
>> duplicated and freed in spi_nor_free()?
>>
>> [ ... ]
>>
> 
> Agreed. Duplicating the name at probe and freeing it with the spi_nor
> would fix it. Left for a follow-up.
> 
> Happy to send these as a separate series if the maintainers prefer.
> 
> Thanks,
> Tze Yee




More information about the linux-mtd mailing list