-
Flawed Disjoint-Device Validation in pool_ctr:
In drivers/md/dm-thin.c:pool_ctr(), thin pools require that the metadata device (argv[0]) and data device (argv[1]) be separate, distinct block devices. The constructor checks:
/* make sure metadata and data are different devices */
if (!strcmp(argv[0], argv[1])) {
ti->error = "Error setting metadata or data device";
r = -EINVAL;
goto out_unlock;
}
This string check (!strcmp) is insufficient. If a user supplies two syntactically different path strings (e.g. /dev/sda and 8:0, or symlinks / alternative device node paths) that resolve to the same underlying dev_t / block device, the check passes.
-
Mode Asymmetry and upgrade_mode():
When the read_only pool feature is requested (pf.mode == PM_READ_ONLY), the metadata device is opened with metadata_mode = BLK_OPEN_READ:
r = dm_get_device(ti, argv[0], metadata_mode, &metadata_dev);
This allocates and returns a struct table_device td (holding td->dm_dev) with td->count == 1. At this moment, metadata_dev->bdev is valid, allowing warn_if_metadata_device_too_big(metadata_dev->bdev) to succeed.
Next, pool_ctr() opens the data device with read-write access:
r = dm_get_device(ti, argv[1], BLK_OPEN_READ | BLK_OPEN_WRITE, &data_dev);
Because both paths resolve to the same dev_t, dm_get_device() locates the existing struct dm_dev_internal dd in ti->table->devices. Finding that the existing mode is BLK_OPEN_READ while the newly requested mode is BLK_OPEN_READ | BLK_OPEN_WRITE, it invokes upgrade_mode(dd, mode, t->md).
-
Use-After-Free via upgrade_mode():
In drivers/md/dm-table.c:upgrade_mode():
old_dev = dd->dm_dev;
r = dm_get_table_device(md, dd->dm_dev->bdev->bd_dev,
dd->dm_dev->mode | new_mode, &new_dev);
if (r)
return r;
dd->dm_dev = new_dev;
dm_put_table_device(md, old_dev);
dm_get_table_device() looks for an entry in md->table_devices matching (dev, mode). Because the mode differs, it opens a new struct table_device (new_dev). Then, dm_put_table_device(md, old_dev) decrements the refcount of old_dev. Since td->count drops to zero, close_table_device() is called, unlinking and freeing td via kfree(td).
However, metadata_dev in pool_ctr() still points directly to old_dev (&td->dm_dev). It is now a dangling pointer to freed slab memory.
-
Propagation to dm_bufio_client_create and Crash:
Later in pool_ctr():
pool = __pool_find(dm_table_get_md(ti->table), metadata_dev->bdev, data_dev->bdev,
block_size, pf.mode == PM_READ_ONLY, &ti->error, &pool_created);
Reading metadata_dev->bdev performs a read from freed memory. Because the slab entry was cleared / zeroed upon free, metadata_dev->bdev evaluates to NULL (0x00000000).
This NULL pointer is passed down the call chain:
pool_create() $\to$ dm_pool_metadata_open() $\to$ dm_block_manager_create() $\to$ dm_bufio_client_create(bdev, ...).
In drivers/md/dm-bufio.c:dm_bufio_client_create():
c->shrinker = shrinker_alloc(0, "dm-bufio:(%u:%u)",
MAJOR(bdev->bd_dev), MINOR(bdev->bd_dev));
Evaluating bdev->bd_dev reads offset 36 (0x24) from NULL (r3: 00000000), generating the instruction ldr r2, [r3, #36] and triggering the observed kernel NULL pointer dereference crash.