mirror of
https://github.com/linux-msm/laptops-kernel.git
synced 2026-08-13 14:19:53 -07:00
dm-integrity: replace forgeable discard filler with a keyed sector marker
The discard-block check in dm_integrity_rw_tag() treats a stored tag
of all 0xf6 bytes (DISCARD_FILLER) as proof a block was discarded and
skips HMAC verification. allow_discards is only accepted in
dm-integrity's standalone mode. An attacker with raw write access to
the backing device, but without the integrity key, can stamp any block
with an all-0xf6 tag and have it served as authentic.
Add a new "allow_discards_keyed" target argument that marks discarded
blocks with a keyed checksum of (salt || sector) instead, computed by
integrity_discard_checksum().
Fixes: 84597a44a9 ("dm integrity: add optional discard support")
Co-developed-by: Jo Van Bulck <jo.vanbulck@cs.kuleuven.be>
Signed-off-by: Jo Van Bulck <jo.vanbulck@cs.kuleuven.be>
Signed-off-by: Shukai Ni <shukai.ni@kuleuven.be>
Signed-off-by: Mikulas Patocka <mpatocka@redhat.com>
This commit is contained in:
committed by
Mikulas Patocka
parent
fb582397cf
commit
90a4fd32bb
@@ -424,7 +424,8 @@ section above) has the following data format for 'integrity' target.
|
||||
|
||||
target_attributes := <target_name> "," <target_version> "," <dev_name> "," <start>
|
||||
<tag_size> "," <mode> "," [<meta_device> ","] [<block_size> ","] <recalculate> ","
|
||||
<allow_discards> "," <fix_padding> "," <fix_hmac> "," <legacy_recalculate> ","
|
||||
<allow_discards> "," <allow_discards_keyed> "," <fix_padding> "," <fix_hmac> ","
|
||||
<legacy_recalculate> ","
|
||||
<journal_sectors> "," <interleave_sectors> "," <buffer_sectors> ";"
|
||||
|
||||
target_name := "target_name=integrity"
|
||||
@@ -438,6 +439,7 @@ section above) has the following data format for 'integrity' target.
|
||||
block_size := "block_size=" <N>
|
||||
recalculate := "recalculate=" <yes_no>
|
||||
allow_discards := "allow_discards=" <yes_no>
|
||||
allow_discards_keyed := "allow_discards_keyed=" <yes_no>
|
||||
fix_padding := "fix_padding=" <yes_no>
|
||||
fix_hmac := "fix_hmac=" <yes_no>
|
||||
legacy_recalculate := "legacy_recalculate=" <yes_no>
|
||||
@@ -455,7 +457,8 @@ section above) has the following data format for 'integrity' target.
|
||||
dm_version=4.45.0;
|
||||
name=integrity1,uuid=,major=253,minor=1,minor_count=1,num_targets=1;
|
||||
target_index=0,target_begin=0,target_len=7856,target_name=integrity,target_version=1.10.0,
|
||||
dev_name=253:0,start=0,tag_size=32,mode=J,recalculate=n,allow_discards=n,fix_padding=n,
|
||||
dev_name=253:0,start=0,tag_size=32,mode=J,recalculate=n,allow_discards=n,
|
||||
allow_discards_keyed=n,fix_padding=n,
|
||||
fix_hmac=n,legacy_recalculate=n,journal_sectors=88,interleave_sectors=32768,buffer_sectors=128;
|
||||
|
||||
|
||||
|
||||
@@ -190,6 +190,19 @@ allow_discards
|
||||
Allow block discard requests (a.k.a. TRIM) for the integrity device.
|
||||
Discards are only allowed to devices using internal hash.
|
||||
|
||||
A discarded block is marked with a constant filler tag that anyone
|
||||
with raw write access to the backing device can forge without the
|
||||
key. Use allow_discards_keyed instead on new volumes.
|
||||
|
||||
allow_discards_keyed
|
||||
Like allow_discards, but marks a discarded block with a keyed
|
||||
checksum of the sector number, HMAC_key(salt || sector), instead of
|
||||
the constant filler tag, so it can't be forged without the
|
||||
integrity key.
|
||||
|
||||
Not compatible with volumes that already have discarded blocks
|
||||
marked the old way; only use on a freshly formatted volume.
|
||||
|
||||
fix_padding
|
||||
Use a smaller padding of the tag area that is more
|
||||
space-efficient. If this option is not present, large padding is
|
||||
|
||||
+113
-22
@@ -66,6 +66,7 @@
|
||||
#define SB_VERSION_4 4
|
||||
#define SB_VERSION_5 5
|
||||
#define SB_VERSION_6 6
|
||||
#define SB_VERSION_7 7
|
||||
#define SB_SECTORS 8
|
||||
#define MAX_SECTORS_PER_BLOCK 8
|
||||
|
||||
@@ -91,6 +92,7 @@ struct superblock {
|
||||
#define SB_FLAG_FIXED_PADDING 0x8
|
||||
#define SB_FLAG_FIXED_HMAC 0x10
|
||||
#define SB_FLAG_INLINE 0x20
|
||||
#define SB_FLAG_DISCARD_KEYED 0x40
|
||||
|
||||
#define JOURNAL_ENTRY_ROUNDUP 8
|
||||
|
||||
@@ -277,6 +279,7 @@ struct dm_integrity_c {
|
||||
bool recalculate_flag;
|
||||
bool reset_recalculate_flag;
|
||||
bool discard;
|
||||
bool discard_keyed;
|
||||
bool fix_padding;
|
||||
bool fix_hmac;
|
||||
bool legacy_recalculate;
|
||||
@@ -483,7 +486,9 @@ static void wraparound_section(struct dm_integrity_c *ic, unsigned int *sec_ptr)
|
||||
|
||||
static void sb_set_version(struct dm_integrity_c *ic)
|
||||
{
|
||||
if (ic->sb->flags & cpu_to_le32(SB_FLAG_INLINE))
|
||||
if (ic->sb->flags & cpu_to_le32(SB_FLAG_DISCARD_KEYED))
|
||||
ic->sb->version = SB_VERSION_7;
|
||||
else if (ic->sb->flags & cpu_to_le32(SB_FLAG_INLINE))
|
||||
ic->sb->version = SB_VERSION_6;
|
||||
else if (ic->sb->flags & cpu_to_le32(SB_FLAG_FIXED_HMAC))
|
||||
ic->sb->version = SB_VERSION_5;
|
||||
@@ -1416,7 +1421,7 @@ static int dm_integrity_rw_tag(struct dm_integrity_c *ic, unsigned char *tag, se
|
||||
{
|
||||
unsigned int hash_offset = 0;
|
||||
unsigned char mismatch_hash = 0;
|
||||
unsigned char mismatch_filler = !ic->discard;
|
||||
unsigned char mismatch_filler = !ic->discard || ic->discard_keyed;
|
||||
|
||||
do {
|
||||
unsigned char *data, *dp;
|
||||
@@ -1468,7 +1473,7 @@ thorough_test:
|
||||
}
|
||||
hash_offset = 0;
|
||||
mismatch_hash = 0;
|
||||
mismatch_filler = !ic->discard;
|
||||
mismatch_filler = !ic->discard || ic->discard_keyed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1646,7 +1651,8 @@ static void integrity_end_io(struct bio *bio)
|
||||
}
|
||||
|
||||
static void integrity_sector_checksum_shash(struct dm_integrity_c *ic, sector_t sector,
|
||||
const char *data, unsigned offset, char *result)
|
||||
const char *data, unsigned offset,
|
||||
unsigned int len, char *result)
|
||||
{
|
||||
__le64 sector_le = cpu_to_le64(sector);
|
||||
SHASH_DESC_ON_STACK(req, ic->internal_shash);
|
||||
@@ -1675,10 +1681,12 @@ static void integrity_sector_checksum_shash(struct dm_integrity_c *ic, sector_t
|
||||
goto failed;
|
||||
}
|
||||
|
||||
r = crypto_shash_update(req, data + offset, ic->sectors_per_block << SECTOR_SHIFT);
|
||||
if (unlikely(r < 0)) {
|
||||
dm_integrity_io_error(ic, "crypto_shash_update", r);
|
||||
goto failed;
|
||||
if (likely(len)) {
|
||||
r = crypto_shash_update(req, data + offset, len);
|
||||
if (unlikely(r < 0)) {
|
||||
dm_integrity_io_error(ic, "crypto_shash_update", r);
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
|
||||
r = crypto_shash_final(req, result);
|
||||
@@ -1699,7 +1707,8 @@ failed:
|
||||
}
|
||||
|
||||
static void integrity_sector_checksum_ahash(struct dm_integrity_c *ic, struct ahash_request **ahash_req,
|
||||
sector_t sector, struct page *page, unsigned offset, char *result)
|
||||
sector_t sector, struct page *page, unsigned offset,
|
||||
unsigned int len, char *result)
|
||||
{
|
||||
__le64 sector_le = cpu_to_le64(sector);
|
||||
struct ahash_request *req;
|
||||
@@ -1708,6 +1717,7 @@ static void integrity_sector_checksum_ahash(struct dm_integrity_c *ic, struct ah
|
||||
int r;
|
||||
unsigned int digest_size;
|
||||
unsigned int nbytes = 0;
|
||||
unsigned int nents = 1 + (len ? 1 : 0);
|
||||
|
||||
might_sleep();
|
||||
|
||||
@@ -1721,12 +1731,12 @@ static void integrity_sector_checksum_ahash(struct dm_integrity_c *ic, struct ah
|
||||
ahash_request_set_callback(req, CRYPTO_TFM_REQ_MAY_SLEEP, crypto_req_done, &wait);
|
||||
|
||||
if (ic->sb->flags & cpu_to_le32(SB_FLAG_FIXED_HMAC)) {
|
||||
sg_init_table(sg, 3);
|
||||
sg_init_table(sg, nents + 1);
|
||||
sg_set_buf(s, (const __u8 *)&ic->sb->salt, SALT_SIZE);
|
||||
nbytes += SALT_SIZE;
|
||||
s++;
|
||||
} else {
|
||||
sg_init_table(sg, 2);
|
||||
sg_init_table(sg, nents);
|
||||
}
|
||||
|
||||
if (likely(!is_vmalloc_addr(§or_le))) {
|
||||
@@ -1739,8 +1749,10 @@ static void integrity_sector_checksum_ahash(struct dm_integrity_c *ic, struct ah
|
||||
nbytes += sizeof(sector_le);
|
||||
s++;
|
||||
|
||||
sg_set_page(s, page, ic->sectors_per_block << SECTOR_SHIFT, offset);
|
||||
nbytes += ic->sectors_per_block << SECTOR_SHIFT;
|
||||
if (likely(len)) {
|
||||
sg_set_page(s, page, len, offset);
|
||||
nbytes += len;
|
||||
}
|
||||
|
||||
ahash_request_set_crypt(req, sg, result, nbytes);
|
||||
|
||||
@@ -1764,10 +1776,40 @@ failed:
|
||||
static void integrity_sector_checksum(struct dm_integrity_c *ic, struct ahash_request **ahash_req,
|
||||
sector_t sector, const char *data, unsigned offset, char *result)
|
||||
{
|
||||
unsigned int len = ic->sectors_per_block << SECTOR_SHIFT;
|
||||
|
||||
if (likely(ic->internal_shash != NULL))
|
||||
integrity_sector_checksum_shash(ic, sector, data, offset, result);
|
||||
integrity_sector_checksum_shash(ic, sector, data, offset, len, result);
|
||||
else
|
||||
integrity_sector_checksum_ahash(ic, ahash_req, sector, (struct page *)data, offset, result);
|
||||
integrity_sector_checksum_ahash(ic, ahash_req, sector, (struct page *)data,
|
||||
offset, len, result);
|
||||
}
|
||||
|
||||
/*
|
||||
* Authenticated marker for a discarded block: HMAC_key(salt || sector), with
|
||||
* no data payload. Because a real data tag's input always covers a full
|
||||
* block, its length differs from this marker's, so the two can never
|
||||
* collide structurally, regardless of block content.
|
||||
*/
|
||||
static void integrity_discard_checksum(struct dm_integrity_c *ic, struct ahash_request **ahash_req,
|
||||
sector_t sector, char *result)
|
||||
{
|
||||
if (likely(ic->internal_shash != NULL))
|
||||
integrity_sector_checksum_shash(ic, sector, NULL, 0, 0, result);
|
||||
else
|
||||
integrity_sector_checksum_ahash(ic, ahash_req, sector, NULL, 0, 0, result);
|
||||
}
|
||||
|
||||
static void integrity_discard_fill_tags(struct dm_integrity_c *ic, struct ahash_request **ahash_req,
|
||||
unsigned char *checksums, sector_t *sector,
|
||||
unsigned int blocks)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
for (i = 0; i < blocks; i++) {
|
||||
integrity_discard_checksum(ic, ahash_req, *sector, checksums + i * ic->tag_size);
|
||||
*sector += ic->sectors_per_block;
|
||||
}
|
||||
}
|
||||
|
||||
static void *integrity_kmap(struct dm_integrity_c *ic, struct page *p)
|
||||
@@ -1796,6 +1838,29 @@ static void *integrity_identity(struct dm_integrity_c *ic, void *data)
|
||||
return virt_to_page(data);
|
||||
}
|
||||
|
||||
static int integrity_recheck_verify_tag(struct dm_integrity_io *dio, char *checksum,
|
||||
char *on_disk_tag, sector_t logical_sector)
|
||||
{
|
||||
struct dm_integrity_c *ic = dio->ic;
|
||||
int r;
|
||||
|
||||
if (!ic->discard_keyed)
|
||||
return dm_integrity_rw_tag(ic, checksum, &dio->metadata_block,
|
||||
&dio->metadata_offset, ic->tag_size, TAG_CMP);
|
||||
|
||||
r = dm_integrity_rw_tag(ic, on_disk_tag, &dio->metadata_block,
|
||||
&dio->metadata_offset, ic->tag_size, TAG_READ);
|
||||
if (unlikely(r))
|
||||
return r;
|
||||
|
||||
r = crypto_memneq(on_disk_tag, checksum, ic->tag_size);
|
||||
if (unlikely(r)) {
|
||||
integrity_discard_checksum(ic, &dio->ahash_req, logical_sector, checksum);
|
||||
r = crypto_memneq(on_disk_tag, checksum, ic->tag_size);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static noinline void integrity_recheck(struct dm_integrity_io *dio, char *checksum)
|
||||
{
|
||||
struct bio *bio = dm_bio_from_per_bio_data(dio, sizeof(struct dm_integrity_io));
|
||||
@@ -1821,6 +1886,7 @@ static noinline void integrity_recheck(struct dm_integrity_io *dio, char *checks
|
||||
char *mem;
|
||||
char *buffer = page_to_virt(page);
|
||||
unsigned int buffer_offset;
|
||||
char on_disk_tag[MAX_T(size_t, HASH_MAX_DIGESTSIZE, MAX_TAG_SIZE)];
|
||||
int r;
|
||||
struct dm_io_request io_req;
|
||||
struct dm_io_region io_loc;
|
||||
@@ -1848,8 +1914,8 @@ static noinline void integrity_recheck(struct dm_integrity_io *dio, char *checks
|
||||
}
|
||||
|
||||
integrity_sector_checksum(ic, &dio->ahash_req, logical_sector, integrity_identity(ic, buffer), buffer_offset, checksum);
|
||||
r = dm_integrity_rw_tag(ic, checksum, &dio->metadata_block,
|
||||
&dio->metadata_offset, ic->tag_size, TAG_CMP);
|
||||
r = integrity_recheck_verify_tag(dio, checksum, on_disk_tag,
|
||||
logical_sector);
|
||||
if (r) {
|
||||
if (r > 0) {
|
||||
DMERR_LIMIT("%pg: Checksum failed at sector 0x%llx",
|
||||
@@ -1915,13 +1981,18 @@ static void integrity_metadata(struct work_struct *w)
|
||||
unsigned int bi_size = dio->bio_details.bi_iter.bi_size;
|
||||
unsigned int max_size = likely(checksums != checksums_onstack) ? PAGE_SIZE : HASH_MAX_DIGESTSIZE;
|
||||
unsigned int max_blocks = max_size / ic->tag_size;
|
||||
sector_t sector = dio->range.logical_sector;
|
||||
|
||||
memset(checksums, DISCARD_FILLER, max_size);
|
||||
if (!ic->discard_keyed)
|
||||
memset(checksums, DISCARD_FILLER, max_size);
|
||||
|
||||
while (bi_size) {
|
||||
unsigned int this_step_blocks = bi_size >> (SECTOR_SHIFT + ic->sb->log2_sectors_per_block);
|
||||
|
||||
this_step_blocks = min(this_step_blocks, max_blocks);
|
||||
if (ic->discard_keyed)
|
||||
integrity_discard_fill_tags(ic, &dio->ahash_req, checksums,
|
||||
§or, this_step_blocks);
|
||||
r = dm_integrity_rw_tag(ic, checksums, &dio->metadata_block, &dio->metadata_offset,
|
||||
this_step_blocks * ic->tag_size, TAG_WRITE);
|
||||
if (unlikely(r)) {
|
||||
@@ -3946,7 +4017,8 @@ static void dm_integrity_status(struct dm_target *ti, status_type_t type,
|
||||
arg_count += ic->sectors_per_block != 1;
|
||||
arg_count += !!(ic->sb->flags & cpu_to_le32(SB_FLAG_RECALCULATING));
|
||||
arg_count += ic->reset_recalculate_flag;
|
||||
arg_count += ic->discard;
|
||||
arg_count += ic->discard && !ic->discard_keyed;
|
||||
arg_count += ic->discard_keyed;
|
||||
arg_count += ic->mode != 'I'; /* interleave_sectors */
|
||||
arg_count += ic->mode == 'J'; /* journal_sectors */
|
||||
arg_count += ic->mode == 'J'; /* journal_watermark */
|
||||
@@ -3969,8 +4041,10 @@ static void dm_integrity_status(struct dm_target *ti, status_type_t type,
|
||||
DMEMIT(" recalculate");
|
||||
if (ic->reset_recalculate_flag)
|
||||
DMEMIT(" reset_recalculate");
|
||||
if (ic->discard)
|
||||
if (ic->discard && !ic->discard_keyed)
|
||||
DMEMIT(" allow_discards");
|
||||
if (ic->discard_keyed)
|
||||
DMEMIT(" allow_discards_keyed");
|
||||
if (ic->mode != 'I')
|
||||
DMEMIT(" interleave_sectors:%u", 1U << ic->sb->log2_interleave_sectors);
|
||||
DMEMIT(" buffer_sectors:%u", 1U << ic->log2_buffer_sectors);
|
||||
@@ -4020,6 +4094,7 @@ static void dm_integrity_status(struct dm_target *ti, status_type_t type,
|
||||
DMEMIT(",recalculate=%c", (ic->sb->flags & cpu_to_le32(SB_FLAG_RECALCULATING)) ?
|
||||
'y' : 'n');
|
||||
DMEMIT(",allow_discards=%c", ic->discard ? 'y' : 'n');
|
||||
DMEMIT(",allow_discards_keyed=%c", ic->discard_keyed ? 'y' : 'n');
|
||||
DMEMIT(",fix_padding=%c",
|
||||
((ic->sb->flags & cpu_to_le32(SB_FLAG_FIXED_PADDING)) != 0) ? 'y' : 'n');
|
||||
DMEMIT(",fix_hmac=%c",
|
||||
@@ -4177,6 +4252,9 @@ static int initialize_superblock(struct dm_integrity_c *ic,
|
||||
get_random_bytes(ic->sb->salt, SALT_SIZE);
|
||||
}
|
||||
|
||||
if (ic->discard_keyed)
|
||||
ic->sb->flags |= cpu_to_le32(SB_FLAG_DISCARD_KEYED);
|
||||
|
||||
if (!ic->meta_dev) {
|
||||
if (ic->fix_padding)
|
||||
ic->sb->flags |= cpu_to_le32(SB_FLAG_FIXED_PADDING);
|
||||
@@ -4835,6 +4913,9 @@ static int dm_integrity_ctr(struct dm_target *ti, unsigned int argc, char **argv
|
||||
ic->reset_recalculate_flag = true;
|
||||
} else if (!strcmp(opt_string, "allow_discards")) {
|
||||
ic->discard = true;
|
||||
} else if (!strcmp(opt_string, "allow_discards_keyed")) {
|
||||
ic->discard = true;
|
||||
ic->discard_keyed = true;
|
||||
} else if (!strcmp(opt_string, "fix_padding")) {
|
||||
ic->fix_padding = true;
|
||||
} else if (!strcmp(opt_string, "fix_hmac")) {
|
||||
@@ -4963,6 +5044,11 @@ static int dm_integrity_ctr(struct dm_target *ti, unsigned int argc, char **argv
|
||||
ti->error = "Discard can be only used with internal hash";
|
||||
goto bad;
|
||||
}
|
||||
if (ic->discard_keyed && !ic->internal_hash_alg.key) {
|
||||
r = -EINVAL;
|
||||
ti->error = "Keyed discard can only be used with keyed internal hash";
|
||||
goto bad;
|
||||
}
|
||||
|
||||
ic->autocommit_jiffies = msecs_to_jiffies(sync_msec);
|
||||
ic->autocommit_msec = sync_msec;
|
||||
@@ -5081,7 +5167,7 @@ static int dm_integrity_ctr(struct dm_target *ti, unsigned int argc, char **argv
|
||||
should_write_sb = true;
|
||||
}
|
||||
|
||||
if (!ic->sb->version || ic->sb->version > SB_VERSION_6) {
|
||||
if (!ic->sb->version || ic->sb->version > SB_VERSION_7) {
|
||||
r = -EINVAL;
|
||||
ti->error = "Unknown version";
|
||||
goto bad;
|
||||
@@ -5129,6 +5215,11 @@ static int dm_integrity_ctr(struct dm_target *ti, unsigned int argc, char **argv
|
||||
goto bad;
|
||||
}
|
||||
}
|
||||
if (!!(ic->sb->flags & cpu_to_le32(SB_FLAG_DISCARD_KEYED)) != ic->discard_keyed) {
|
||||
r = -EINVAL;
|
||||
ti->error = "Mismatch in the discard_keyed flag";
|
||||
goto bad;
|
||||
}
|
||||
if (!!(ic->sb->flags & cpu_to_le32(SB_FLAG_HAVE_JOURNAL_MAC)) != !!ic->journal_mac_alg.alg_string) {
|
||||
r = -EINVAL;
|
||||
ti->error = "Journal mac mismatch";
|
||||
@@ -5444,7 +5535,7 @@ static void dm_integrity_dtr(struct dm_target *ti)
|
||||
|
||||
static struct target_type integrity_target = {
|
||||
.name = "integrity",
|
||||
.version = {1, 14, 0},
|
||||
.version = {1, 15, 0},
|
||||
.module = THIS_MODULE,
|
||||
.features = DM_TARGET_SINGLETON | DM_TARGET_INTEGRITY,
|
||||
.ctr = dm_integrity_ctr,
|
||||
|
||||
Reference in New Issue
Block a user