Skip to content

Commit 4b51429

Browse files
ntsironsnitm
authored andcommitted
dm clone: Fix handling of partial region discards
There is a bug in the way dm-clone handles discards, which can lead to discarding the wrong blocks or trying to discard blocks beyond the end of the device. This could lead to data corruption, if the destination device indeed discards the underlying blocks, i.e., if the discard operation results in the original contents of a block to be lost. The root of the problem is the code that calculates the range of regions covered by a discard request and decides which regions to discard. Since dm-clone handles the device in units of regions, we don't discard parts of a region, only whole regions. The range is calculated as: rs = dm_sector_div_up(bio->bi_iter.bi_sector, clone->region_size); re = bio_end_sector(bio) >> clone->region_shift; , where 'rs' is the first region to discard and (re - rs) is the number of regions to discard. The bug manifests when we try to discard part of a single region, i.e., when we try to discard a block with size < region_size, and the discard request both starts at an offset with respect to the beginning of that region and ends before the end of the region. The root cause is the following comparison: if (rs == re) // skip discard and complete original bio immediately , which doesn't take into account that 'rs' might be greater than 're'. Thus, we then issue a discard request for the wrong blocks, instead of skipping the discard all together. Fix the check to also take into account the above case, so we don't end up discarding the wrong blocks. Also, add some range checks to dm_clone_set_region_hydrated() and dm_clone_cond_set_range(), which update dm-clone's region bitmap. Note that the aforementioned bug doesn't cause invalid memory accesses, because dm_clone_is_range_hydrated() returns True for this case, so the checks are just precautionary. Fixes: 7431b78 ("dm: add clone target") Cc: [email protected] # v5.4+ Signed-off-by: Nikos Tsironis <[email protected]> Signed-off-by: Mike Snitzer <[email protected]>
1 parent 1edaa44 commit 4b51429

File tree

2 files changed

+42
-14
lines changed

2 files changed

+42
-14
lines changed

drivers/md/dm-clone-metadata.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,12 @@ int dm_clone_set_region_hydrated(struct dm_clone_metadata *cmd, unsigned long re
850850
struct dirty_map *dmap;
851851
unsigned long word, flags;
852852

853+
if (unlikely(region_nr >= cmd->nr_regions)) {
854+
DMERR("Region %lu out of range (total number of regions %lu)",
855+
region_nr, cmd->nr_regions);
856+
return -ERANGE;
857+
}
858+
853859
word = region_nr / BITS_PER_LONG;
854860

855861
spin_lock_irqsave(&cmd->bitmap_lock, flags);
@@ -879,6 +885,13 @@ int dm_clone_cond_set_range(struct dm_clone_metadata *cmd, unsigned long start,
879885
struct dirty_map *dmap;
880886
unsigned long word, region_nr;
881887

888+
if (unlikely(start >= cmd->nr_regions || (start + nr_regions) < start ||
889+
(start + nr_regions) > cmd->nr_regions)) {
890+
DMERR("Invalid region range: start %lu, nr_regions %lu (total number of regions %lu)",
891+
start, nr_regions, cmd->nr_regions);
892+
return -ERANGE;
893+
}
894+
882895
spin_lock_irq(&cmd->bitmap_lock);
883896

884897
if (cmd->read_only) {

drivers/md/dm-clone-target.c

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,17 @@ static inline unsigned long bio_to_region(struct clone *clone, struct bio *bio)
293293

294294
/* Get the region range covered by the bio */
295295
static void bio_region_range(struct clone *clone, struct bio *bio,
296-
unsigned long *rs, unsigned long *re)
296+
unsigned long *rs, unsigned long *nr_regions)
297297
{
298+
unsigned long end;
299+
298300
*rs = dm_sector_div_up(bio->bi_iter.bi_sector, clone->region_size);
299-
*re = bio_end_sector(bio) >> clone->region_shift;
301+
end = bio_end_sector(bio) >> clone->region_shift;
302+
303+
if (*rs >= end)
304+
*nr_regions = 0;
305+
else
306+
*nr_regions = end - *rs;
300307
}
301308

302309
/* Check whether a bio overwrites a region */
@@ -454,7 +461,7 @@ static void trim_bio(struct bio *bio, sector_t sector, unsigned int len)
454461

455462
static void complete_discard_bio(struct clone *clone, struct bio *bio, bool success)
456463
{
457-
unsigned long rs, re;
464+
unsigned long rs, nr_regions;
458465

459466
/*
460467
* If the destination device supports discards, remap and trim the
@@ -463,22 +470,31 @@ static void complete_discard_bio(struct clone *clone, struct bio *bio, bool succ
463470
*/
464471
if (test_bit(DM_CLONE_DISCARD_PASSDOWN, &clone->flags) && success) {
465472
remap_to_dest(clone, bio);
466-
bio_region_range(clone, bio, &rs, &re);
473+
bio_region_range(clone, bio, &rs, &nr_regions);
467474
trim_bio(bio, rs << clone->region_shift,
468-
(re - rs) << clone->region_shift);
475+
nr_regions << clone->region_shift);
469476
generic_make_request(bio);
470477
} else
471478
bio_endio(bio);
472479
}
473480

474481
static void process_discard_bio(struct clone *clone, struct bio *bio)
475482
{
476-
unsigned long rs, re;
483+
unsigned long rs, nr_regions;
477484

478-
bio_region_range(clone, bio, &rs, &re);
479-
BUG_ON(re > clone->nr_regions);
485+
bio_region_range(clone, bio, &rs, &nr_regions);
486+
if (!nr_regions) {
487+
bio_endio(bio);
488+
return;
489+
}
480490

481-
if (unlikely(rs == re)) {
491+
if (WARN_ON(rs >= clone->nr_regions || (rs + nr_regions) < rs ||
492+
(rs + nr_regions) > clone->nr_regions)) {
493+
DMERR("%s: Invalid range (%lu + %lu, total regions %lu) for discard (%llu + %u)",
494+
clone_device_name(clone), rs, nr_regions,
495+
clone->nr_regions,
496+
(unsigned long long)bio->bi_iter.bi_sector,
497+
bio_sectors(bio));
482498
bio_endio(bio);
483499
return;
484500
}
@@ -487,7 +503,7 @@ static void process_discard_bio(struct clone *clone, struct bio *bio)
487503
* The covered regions are already hydrated so we just need to pass
488504
* down the discard.
489505
*/
490-
if (dm_clone_is_range_hydrated(clone->cmd, rs, re - rs)) {
506+
if (dm_clone_is_range_hydrated(clone->cmd, rs, nr_regions)) {
491507
complete_discard_bio(clone, bio, true);
492508
return;
493509
}
@@ -1169,7 +1185,7 @@ static void process_deferred_discards(struct clone *clone)
11691185
int r = -EPERM;
11701186
struct bio *bio;
11711187
struct blk_plug plug;
1172-
unsigned long rs, re;
1188+
unsigned long rs, nr_regions;
11731189
struct bio_list discards = BIO_EMPTY_LIST;
11741190

11751191
spin_lock_irq(&clone->lock);
@@ -1185,14 +1201,13 @@ static void process_deferred_discards(struct clone *clone)
11851201

11861202
/* Update the metadata */
11871203
bio_list_for_each(bio, &discards) {
1188-
bio_region_range(clone, bio, &rs, &re);
1204+
bio_region_range(clone, bio, &rs, &nr_regions);
11891205
/*
11901206
* A discard request might cover regions that have been already
11911207
* hydrated. There is no need to update the metadata for these
11921208
* regions.
11931209
*/
1194-
r = dm_clone_cond_set_range(clone->cmd, rs, re - rs);
1195-
1210+
r = dm_clone_cond_set_range(clone->cmd, rs, nr_regions);
11961211
if (unlikely(r))
11971212
break;
11981213
}

0 commit comments

Comments
 (0)