On Tue, Jul 28, 2026 at 8:16 AM Bryam Vargas via B4 Relay
<[email protected]> wrote:
>
> From: Bryam Vargas <[email protected]>
>
> ---
> Found by reading array_block_check() next to node_check(), its opposite 
> number for btree
> nodes in the same directory.  node_check() already bounded max_entries and 
> nr_entries when
> dm-array was added in 2013; the array validator never picked it up.
>
> Reproducer.  Create a cache on a zeroed metadata device so dm-cache formats 
> it, tear that
> down, then set nr_entries in the first mapping array block to 0xFFFFFFFF and 
> recompute the
> block checksum.  dm_bm_checksum() is crc32c(~0, data, len) ^ CSUM_XOR, so the 
> checksum costs
> nothing to forge -- it is a corruption check, not a tag.  The crafted image 
> differs from the
> one the kernel itself wrote by four bytes plus that checksum.
>
> A 4096-byte block with a 24-byte header and 8-byte values holds 509 entries, 
> so index 509 is
> the first one outside it.
>
> A/B on v7.2-rc4-610 with KASAN and kasan.fault=report, this patch built as 
> posted:
>
>   pristine image, unpatched:  activates, walks all 2048 cache blocks, dmesg 
> empty
>   crafted image,  unpatched:  reads past the block, below
>   crafted image,  patched:    "array_block_check failed: too many entries", 
> -EILSEQ
>   pristine image, patched:    activates, dmesg empty
>
>     BUG: KASAN: slab-use-after-free in dm_cache_load_mappings+0x9b1/0xb40 
> [dm_cache]
>     Read of size 8 at addr ffff888107fc8000 by task dmsetup/1667
>      dm_cache_load_mappings+0x9b1/0xb40 [dm_cache]
>      cache_preresume+0x4cd/0xd70 [dm_cache]
>      dm_table_resume_targets+0xcd/0x2e0 [dm_mod]
>      ctl_ioctl+0x512/0xa90 [dm_mod]
>      __x64_sys_ioctl+0x134/0x1c0
>     The buggy address is located 0 bytes inside of
>      freed 32-byte region [ffff888107fc8000, ffff888107fc8020)
>
> The block base was ffff888107fc7000, so that address is base + 0x1000: byte 
> 4096, index 509,
> the first entry outside.
>
> Worth flagging about that KASAN line: it corroborates, it does not reproduce 
> on demand.  Two
> earlier runs of the same arm produced no report at all, even though the 
> overread happened
> both times and dm-cache said so by refusing a nonsense mapping at cache block 
> 509.  dm-bufio
> serves this buffer from __get_free_pages(), the page allocator has no 
> redzones, and byte 4096
> is the first byte of the next page, so KASAN can only speak when that page 
> happens to be
> poisoned.  The first line is the deterministic one.
>
> The check is not conservative.  calc_max_entries() gives 509 for 8-byte 
> values and 1018 for
> 4-byte ones, and 24 + 8*509 and 24 + 4*1018 are both exactly 4096, so 
> metadata dm-array wrote
> sits right at the limit and still passes.

Thank you for the patch. I agree with validating these header fields,
and I have a slightly different proposal: compare max_entries against
"calc_max_entries(value_size, size_of_block)" in array_block_check().
This prevents callers from hitting BUG_ON in fill_ablock() or
trim_ablock() when a max_entries is smaller than the expected
calc_max_entries().

Adding the equality check has no compatibility concerns. dm-array's
calc_max_entries() is unchanged since it was introduced in 2013. Also,
the max_entries is used by dm_array_get_value() to address entries;
any further change to the max_entries formula would break addressing
on existing metadata.

Checking "!value_size" becomes necessary to avoid divide-by-zero in
calc_max_entries(). I'd prefer splitting the value_size check from
max_entries with a different error message for diagnosability. A
combined form "if (!value_size || max_entries != ...)" also works, but
the shared error message needs some tweaks, maybe printing both
value_size and max_entries, not just "max_entries too large".

> There is a second hole I am not fixing here, since it wants its own 
> reproducer and this one is
> headed for stable.  A validator can only check a header against itself, so a 
> block with
> value_size 4 and max_entries 1018 is internally consistent and gets through, 
> but element_at()
> indexes with the caller's value size rather than the stored one -- and 
> dm-cache keeps arrays of
> both, mappings at 8 and hints at 4.  Read an array block of one through the 
> other and the walk
> runs to byte 8160 of a 4096-byte block.  Closing that means comparing 
> value_size against
> info->value_type.size in get_ablock() and in __shadow_ablock(), the two 
> places that hold both
> the block and the caller.
>
> dm-era reaches the same accessor directly and dm-clone reaches it through 
> dm-bitset, which is
> an array underneath; dm-cache is only the shortest path from a crafted image 
> to an observable
> read.  All of them need CAP_SYS_ADMIN to load the table, which is why I'm 
> sending this as
> hardening rather than as a security report.
>
> The value_size patch follows once I have a reproducer for it.

That kind of contextual error is a concern. It could be reproduced by
a crafted metadata with swapped root nodes for mappings and hints in
superblock (with updated checksum). During device activation and
__load_mappings(), dm_array_cursor_next() iterates entries according
to the cached array block's nr_entries, while
dm_array_cursor_get_value() reads the entries in terms of
info->value_size, triggerring out-of-bounds access,

cache_check and era_check v1.0+ already detect value_size
inconsistencies, and older thin-provisioning-tools can detect the
error by checking illegal mappings and flags, that helps mitigate the
risk offline. An in-kernel follow-up patch is still worthwhile for
online validation.


> ---


Reply via email to