]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
dm cache: prevent entering passthrough mode after unclean shutdown
authorMing-Hung Tsai <mtsai@redhat.com>
Mon, 9 Feb 2026 07:54:11 +0000 (15:54 +0800)
committerMikulas Patocka <mpatocka@redhat.com>
Mon, 2 Mar 2026 15:50:18 +0000 (16:50 +0100)
dm-cache assumes all cache blocks are dirty when it recovers from an
unclean shutdown. Given that the passthrough mode doesn't handle dirty
blocks, we should not load a cache in passthrough mode if it was not
cleanly shut down; or we'll risk data loss while updating an actually
dirty block.

Also bump the target version to 2.4.0 to mark completion of passthrough
mode fixes.

Reproduce steps:

1. Create a writeback cache with zero migration_threshold to produce
   dirty blocks.

dmsetup create cmeta --table "0 8192 linear /dev/sdc 0"
dmsetup create cdata --table "0 131072 linear /dev/sdc 8192"
dmsetup create corig --table "0 262144 linear /dev/sdc 262144"
dd if=/dev/zero of=/dev/mapper/cmeta bs=4k count=1 oflag=direct
dmsetup create cache --table "0 262144 cache /dev/mapper/cmeta \
/dev/mapper/cdata /dev/mapper/corig 128 2 metadata2 writeback smq \
2 migration_threshold 0"

2. Write the first cache block dirty

fio --filename=/dev/mapper/cache --name=populate --rw=write --bs=4k \
--direct=1 --size=64k

3. Ensure the number of dirty blocks is 1. This status query triggers
   metadata commit without flushing the dirty bitset, setting up the
   unclean shutdown state.

dmsetup status cache | awk '{print $14}'

4. Force reboot, leaving the cache uncleanly shutdown.

echo b > /proc/sysrq-trigger

5. Activate the above cache components, and verify the first data block
   remains dirty.

dmsetup create cmeta --table "0 8192 linear /dev/sdc 0"
dmsetup create cdata --table "0 131072 linear /dev/sdc 8192"
dmsetup create corig --table "0 262144 linear /dev/sdc 262144"
dd if=/dev/mapper/cdata of=/tmp/cb0.bin bs=64k count=1
dd if=/dev/mapper/corig of=/tmp/ob0.bin bs=64k count=1
md5sum /tmp/cb0.bin /tmp/ob0.bin # expected to be different

6. Try bringing up the cache in passthrough mode. It succeeds, while the
   first cache block was loaded dirty due to unclean shutdown, violates
   the passthrough mode's constraints.

dmsetup create cache --table "0 262144 cache /dev/mapper/cmeta \
/dev/mapper/cdata /dev/mapper/corig 128 2 metadata2 passthrough smq 0"
dmsetup status cache | awk '{print $14}'

7. (Optional) Demonstrate the integrity issue: invalidating the dirty
   block in passthrough mode doesn't write back the dirty data, causing
   data loss.

fio --filename=/dev/mapper/cache --name=invalidate --rw=write --bs=4k \
--direct=1 --size=4k  # overwrite the first 4k to trigger invalidation
dmsetup remove cache
dd if=/dev/mapper/corig of=/tmp/ob0new.bin bs=64k count=1
cb0sum=$(dd if=/tmp/cb0.bin bs=4k count=15 skip=1 | md5sum | \
awk '{print $1}')
ob0newsum=$(dd if=/tmp/ob0new.bin bs=4k count=15 skip=1 | md5sum | \
awk '{print $1}')
echo "$cb0sum, $ob0newsum"  # remaining 60k should differ (data loss)

Signed-off-by: Ming-Hung Tsai <mtsai@redhat.com>
Signed-off-by: Mikulas Patocka <mpatocka@redhat.com>
drivers/md/dm-cache-metadata.c
drivers/md/dm-cache-metadata.h
drivers/md/dm-cache-target.c

index 1b86e80c89cca0caafe1bbc331d5c0721f991c4c..25b8aebdca5342759b2d3db7a0059378d4142785 100644 (file)
@@ -1813,3 +1813,12 @@ out:
 
        return r;
 }
+
+int dm_cache_metadata_clean_when_opened(struct dm_cache_metadata *cmd, bool *result)
+{
+       READ_LOCK(cmd);
+       *result = cmd->clean_when_opened;
+       READ_UNLOCK(cmd);
+
+       return 0;
+}
index 2f107e7c67d0a856f763ba08344de575e8b6e669..91f8706b41fddecb4dcab8369d11239b4db210fe 100644 (file)
@@ -141,6 +141,11 @@ void dm_cache_metadata_set_read_only(struct dm_cache_metadata *cmd);
 void dm_cache_metadata_set_read_write(struct dm_cache_metadata *cmd);
 int dm_cache_metadata_abort(struct dm_cache_metadata *cmd);
 
+/*
+ * Query method.  Was the metadata cleanly shut down when opened?
+ */
+int dm_cache_metadata_clean_when_opened(struct dm_cache_metadata *cmd, bool *result);
+
 /*----------------------------------------------------------------*/
 
 #endif /* DM_CACHE_METADATA_H */
index e479ac22b97cc6cabd5e6e8f70e43848e0752b32..f8200c15480525961d2be72b6171cee6cdbd2779 100644 (file)
@@ -2952,6 +2952,9 @@ static dm_cblock_t get_cache_dev_size(struct cache *cache)
 
 static bool can_resume(struct cache *cache)
 {
+       bool clean_when_opened;
+       int r;
+
        /*
         * Disallow retrying the resume operation for devices that failed the
         * first resume attempt, as the failure leaves the policy object partially
@@ -2968,6 +2971,20 @@ static bool can_resume(struct cache *cache)
                return false;
        }
 
+       if (passthrough_mode(cache)) {
+               r = dm_cache_metadata_clean_when_opened(cache->cmd, &clean_when_opened);
+               if (r) {
+                       DMERR("%s: failed to query metadata flags", cache_device_name(cache));
+                       return false;
+               }
+
+               if (!clean_when_opened) {
+                       DMERR("%s: unable to resume into passthrough mode after unclean shutdown",
+                             cache_device_name(cache));
+                       return false;
+               }
+       }
+
        return true;
 }
 
@@ -3533,7 +3550,7 @@ static void cache_io_hints(struct dm_target *ti, struct queue_limits *limits)
 
 static struct target_type cache_target = {
        .name = "cache",
-       .version = {2, 3, 0},
+       .version = {2, 4, 0},
        .module = THIS_MODULE,
        .ctr = cache_ctr,
        .dtr = cache_dtr,