]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
mtd: fix double free and WARN_ON in add_mtd_device() error paths
authorXue Lei <Xue.Lei@windriver.com>
Wed, 1 Jul 2026 12:10:42 +0000 (20:10 +0800)
committerMiquel Raynal <miquel.raynal@bootlin.com>
Fri, 3 Jul 2026 13:45:43 +0000 (15:45 +0200)
When device_register() or mtd_nvmem_add() fails inside
add_mtd_device() for a partition, the error handling triggers
mtd_release() via put_device() or device_unregister(). mtd_release()
calls release_mtd_partition() which frees the mtd_info structure.
However, callers such as mtd_add_partition() and add_mtd_partitions()
also call free_partition() in their error paths, resulting in a double
free.

Additionally, release_mtd_partition() hits WARN_ON(!list_empty(
&mtd->part.node)) because the partition node is still linked in the
parent's partitions list when the release callback fires from the
add_mtd_device() error path.

Fix this by overriding dev->type and dev->release before put_device()
in the error paths, so that device_release() invokes a no-op function
instead of mtd_release(). For the mtd_nvmem_add() failure case,
device_unregister() is replaced with device_del() to separate the
device removal from the final kobject reference drop, allowing the
override to take effect before put_device() is called.

The callers' error paths (list_del + free_partition) remain the sole
owners of mtd_info lifetime on add_mtd_device() failure, which is the
expected contract.

The normal partition teardown path is not affected: del_mtd_device()
goes through kref_put() -> mtd_device_release() -> device_unregister()
with dev->type still set to &mtd_devtype, so mtd_release() ->
release_mtd_partition() continues to work correctly for the regular
removal case.

Reported-by: syzbot+e9c76b56dc05023b8117@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=e9c76b56dc05023b8117
Fixes: 19bfa9ebebb5 ("mtd: use refcount to prevent corruption")
Signed-off-by: Xue Lei <Xue.Lei@windriver.com>
Signed-off-by: Miquel Raynal <miquel.raynal@bootlin.com>
drivers/mtd/mtdcore.c

index 5765377746288782d6ea5e19b06bdabfad91f2c0..16629382a787bd5882102ce4373fa93259c73690 100644 (file)
@@ -105,6 +105,15 @@ static void mtd_release(struct device *dev)
        device_destroy(&mtd_class, index + 1);
 }
 
+/*
+ * No-op device release used in add_mtd_device() error paths.
+ * Prevents mtd_release() from being called via device_release(),
+ * which would free the mtd_info that the caller still manages.
+ */
+static void mtd_dev_release_nop(struct device *dev)
+{
+}
+
 static void mtd_device_release(struct kref *kref)
 {
        struct mtd_info *mtd = container_of(kref, struct mtd_info, refcnt);
@@ -799,10 +808,8 @@ int add_mtd_device(struct mtd_info *mtd)
        mtd_check_of_node(mtd);
        of_node_get(mtd_get_of_node(mtd));
        error = device_register(&mtd->dev);
-       if (error) {
-               put_device(&mtd->dev);
+       if (error)
                goto fail_added;
-       }
 
        /* Add the nvmem provider */
        error = mtd_nvmem_add(mtd);
@@ -840,8 +847,16 @@ int add_mtd_device(struct mtd_info *mtd)
        return 0;
 
 fail_nvmem_add:
-       device_unregister(&mtd->dev);
+       device_del(&mtd->dev);
 fail_added:
+       /*
+        * Clear type and set nop release to prevent mtd_release() ->
+        * release_mtd_partition() -> free_partition() from freeing mtd.
+        * The caller handles cleanup on failure.
+        */
+       mtd->dev.type = NULL;
+       mtd->dev.release = mtd_dev_release_nop;
+       put_device(&mtd->dev);
        of_node_put(mtd_get_of_node(mtd));
 fail_devname:
        idr_remove(&mtd_idr, i);