syzbot
WARNING: zero-size vmalloc in cache_ctr
Status:
upstream: reported C repro on 2026/10/31 17:03
Subsystems:
dm
Labels:
prio:low
[Documentation on labels]
Reported-by: syzbot+c8564fc0a96e3ecbe4d4@syzkaller.appspotmail.com
First crash: 4d06h, last: 1d19h
✨ AI-generated root cause summary:
In drivers/md/dm-cache-target.c, calculate_discard_block_size() doubles discard_block_size beyond 32 bits when origin_sectors is very large, and calls sector_div() which truncates the divisor to a 32-bit uint32_t, resulting in division by zero. On arm64, division by zero architecturally yields 0, setting discard_nr_blocks to 0 and triggering a zero-size vzalloc() that hits WARN_ON_ONCE(!size) in __vmalloc_node_range_noprof; the error is subsequently caught by dm-cache-target and cleanly returned as -ENOMEM. On x86_64, the same division by zero triggers an unhandled #DE divide error kernel oops.
▶
▼
✨ Full AI root cause analysis
Root Cause Analysis
1. Involved Functions, Structs, and Files
- Files:
- Functions & Structs:
struct cache: Fields cache->origin_sectors (sector_t), cache->discard_block_size (uint32_t), cache->discard_nr_blocks (dm_dblock_t), cache->discard_bitset (unsigned long *).
calculate_discard_block_size() / too_many_discard_blocks(): Computes discard block size such that origin sectors divided by discard block size does not exceed MAX_DISCARD_BLOCKS (16384).
dm_sector_div_up(): Computes ceil(n / sz) using sector_div().
alloc_bitset(): Computes bytes needed for a bitset and invokes vzalloc().
__vmalloc_node_range_noprof(): Checks WARN_ON_ONCE(!size).
2. Trigger Mechanism and Bug Flow
-
Large Origin Size Specification:
During a DM_TABLE_LOAD ioctl, a cache target is created with a very large target sector length (cache->origin_sectors = ti->len = 0x3fffffffffffff) and block size ca->block_size = 1024.
-
Discard Block Size Calculation Loop:
In cache_create() (drivers/md/dm-cache-target.c:2524), the kernel calculates:
cache->discard_block_size =
calculate_discard_block_size(cache->sectors_per_block, cache->origin_sectors);
calculate_discard_block_size() doubles discard_block_size while too_many_discard_blocks(discard_block_size, origin_size) returns true:
static bool too_many_discard_blocks(sector_t discard_block_size, sector_t origin_size)
{
(void) sector_div(origin_size, discard_block_size);
return origin_size > MAX_DISCARD_BLOCKS;
}
Because origin_sectors is huge (0x3fffffffffffff), discard_block_size (starting at 1024) is doubled in a loop. Even at 1ULL << 31 (2147483648), 0x3fffffffffffff / (1ULL << 31) = 0x7fffff = 8388607 > MAX_DISCARD_BLOCKS (16384). Therefore, the loop doubles discard_block_size once more to 1ULL << 32 (0x100000000).
-
Divisor 32-bit Truncation in sector_div():
sector_div(n, base) is defined as do_div(n, base). Per include/asm-generic/div64.h:
#define do_div(n, base) ({ \
uint32_t __base = (base); \
...
The divisor is cast and truncated to a 32-bit uint32_t. When discard_block_size == 1ULL << 32, (uint32_t)(1ULL << 32) == 0.
-
Architectural Divergence on Division by Zero:
- On ARM64 (the architecture of this report):
ARM64 hardware integer division (
udiv) architecturally specifies that division by zero does not trap and returns 0. Consequently, sector_div(origin_size, 0) sets origin_size = 0. The condition 0 > 16384 evaluates to false, exiting the loop and returning 1ULL << 32.
- On x86_64:
Division by zero triggers a CPU Division Error (
#DE) exception, resulting in an immediate kernel panic/oops in kernel mode.
-
Subsequent Zero-Size Allocation on ARM64:
On ARM64, execution continues:
3. Runtime Consequences in a Production Build
- On the corresponding ARM64 production build (without
panic_on_warn):
- On x86_64 production builds:
sector_div with a divisor truncated to zero triggers an unhandled #DE exception and halts execution (kernel panic / oops).
4. Classification Summary
- MemoryCorruption:
false (No memory corruption, out-of-bounds writes, or use-after-free occurs; on ARM64 the allocation returns NULL and errors out, and on x86_64 execution traps on division by zero).
- InfoLeak:
"none" (No kernel memory contents are exposed to userspace).
- RawDenialOfService:
false (On the corresponding ARM64 production build matching the report, execution is not halted: WARN_ON_ONCE(!size) is caught and gracefully handled by cache_create(), unwinding all state and returning -ENOMEM).
- IsNetCandidate:
false (Storage / Device Mapper subsystem; no network stack involvement).
- IsKVMCandidate:
false (No KVM or virtualization stack involvement).
Sample crash report:
* Struck through repros no longer work on HEAD.