Device Mapper
The kernel framework behind LVM, LUKS, and thin pools
The Big Idea
What device-mapper Is
device-mapper (dm) is an in-kernel framework for building virtual block devices. Each virtual device is defined by a table that maps ranges of the logical device (identified by sector offsets) to one or more underlying devices, optionally transforming the I/O as it passes through (encryption, striping, snapshotting, etc.).
You don't normally use device-mapper directly — you use LVM, cryptsetup/LUKS, multipath, thin pools, dm-verity, or lvm-cache. All of those are consumers of dm. They translate high-level intent ("create a logical volume", "encrypt this partition") into dm tables, and let the kernel do the actual I/O plumbing.
Tip
The "aha" moment: LVM, LUKS, thin provisioning, software RAID (via dm-raid), SSD caching, multipath, and read-only integrity verification are not separate subsystems. They are all the same subsystem — device-mapper — with different target modules loaded. Once you internalize dm, the rest of the Linux storage stack collapses into a single mental model.
Why It Exists
Before device-mapper
- LVM1 had its own in-kernel code for mapping logical-to-physical extents
- cryptoloop / loop-AES had their own in-kernel encryption path
- Software RAID (md) lived in its own subsystem
- Each feature reimplemented block-device plumbing: request queues, suspend/resume, hotplug, ioctls
- No composability: you couldn't naturally stack encryption under RAID under LVM without friction
After device-mapper (2.6+)
- A single framework in
drivers/md/dm*.chandles all virtual block devices - Each transformation is a pluggable target (kernel module):
dm-crypt,dm-linear,dm-raid,dm-thin,dm-cache,dm-verity… - Targets compose naturally by stacking dm devices on top of each other
- Uniform userspace interface:
ioctl+dmsetup; uniform sysfs at/sys/block/dm-N/ - LVM2, cryptsetup, multipathd, thin_*, lvm-cache all became userspace controllers that build dm tables
Architecture — Tables, Targets, and Mappings
The Core Model
A dm device is just a table. Each row ("target instance") maps a contiguous range of the logical (virtual) device to a specific target type with its own arguments:
# Table row syntax:
<logical_start_sector> <length_in_sectors> <target_type> <target_specific_args>Sectors are always 512 bytes in dm accounting, regardless of the underlying device's physical sector size. Multiple rows stack vertically to cover the logical device: they must be contiguous (no gaps) and non-overlapping, and together span [0 … total_length).
Example: a logical device assembled from two physical regions
# 20 GiB virtual device "vg00-data"
# sectors 0–10485759 (5 GiB) → /dev/sda3 starting at sector 2048
# sectors 10485760–41943039 (15 GiB) → /dev/sdb1 starting at sector 0
0 10485760 linear /dev/sda3 2048
10485760 31457280 linear /dev/sdb1 0This is literally how LVM builds a linear logical volume spanning two PVs. The userspace lvcreate decides the layout; the kernel only sees the table.
Note
Key insight: LVM's online pvmove works by atomically swapping a dm table. The kernel suspends the device, updates the mapping to point at new extents, resumes, and in-flight I/O is reissued transparently. No filesystem ever notices.
How a Write Travels Through a dm Stack
Consider a typical laptop: ext4 on top of an LVM logical volume, LVM on top of a LUKS-encrypted partition, LUKS on top of an NVMe partition.
ext4 (journaled write, 4 KiB block)dm-linear — LV "root" — translates LV sector → VG extent sectordm-crypt — AES-XTS encrypts each sector with a per-sector tweak/dev/nvme0n1p3 (offset + length on disk)What actually happens on a single write
- ext4 issues a
bio(block I/O request) for logical sector Sfs on/dev/mapper/vg0-root. - Kernel sees the target device is a dm device; the request enters
dm.c. - dm-linear looks up Sfs in its table, picks the matching row, rewrites the
bioto target/dev/mapper/cryptrootat sector Slv, forwards it. - dm-crypt receives the
bio, allocates a bounce page, encrypts each 512-byte sector with AES-XTS using Slv as the tweak, retargets thebioto/dev/nvme0n1p3at Slv + offset, submits it to a workqueue (encryption is async). - The partition layer adds the partition start offset, hands off to the NVMe driver.
- On completion, each dm target's
endiofires in reverse, bubbling the result up to ext4.
The Target Catalog
Each target is a small kernel module (drivers/md/dm-*.c) that implements .ctr (construct from table args), .dtr (destruct), .map (remap a bio), .end_io, and .status. This is the entire contract.
| Target | What it does | Used by |
|---|---|---|
linear |
1:1 map of a range to (device, offset). The workhorse. |
LVM linear LVs, partition emulation, pvmove intermediate |
stripe |
RAID-0 block interleaving across N devices with configurable chunk size. | LVM striped LVs |
mirror |
RAID-1 with a userspace log daemon. Legacy. | Old LVM mirrored LVs (largely replaced by dm-raid) |
raid |
Wraps the kernel MD code to provide RAID 0/1/4/5/6/10 with bitmap, rebuild, and integrity. | LVM RAID (lvcreate --type raid5); shares the MD engine with mdadm |
crypt |
Transparent block-level encryption (AES-XTS, -CBC-ESSIV, Adiantum). Keys held in the kernel keyring. | LUKS1/LUKS2, plain dm-crypt, TrueCrypt/VeraCrypt compat (→ storage-06) |
thin-pool + thin |
Thin provisioning: a pool with metadata + data devices, and thin volumes that allocate blocks on first write. Shared CoW snapshots. | LVM thin, Docker devicemapper graph driver (legacy) (→ storage-05) |
snapshot + snapshot-origin |
Traditional CoW snapshots: origin + separate COW store. Writes to origin copy old blocks into the store first. | Classic LVM snapshots (lvcreate -s, pre-thin) |
cache |
SSD-backed caching of a slow origin device. Policy-driven (smq/mq/cleaner) writeback or writethrough. | LVM cache (lvconvert --cache), standalone dm-cache |
integrity |
Per-block checksum (CRC32C, SHA256, or HMAC) with an internal journal. Detects bit rot; with HMAC it authenticates. | integritysetup, LUKS2 --integrity for authenticated encryption |
verity |
Read-only Merkle-tree integrity: every block's hash is in a hash tree; root hash is signed externally. I/O returns EIO on mismatch. | Android verified boot (AVB), ChromeOS rootfs, confidential containers, OSTree-based immutables |
multipath |
Aggregate multiple physical paths to the same SAN LUN for failover and load balancing. Groups with priorities. | multipathd; Fibre Channel / iSCSI enterprise storage |
zero |
Reads return zeroes; writes are discarded. O(1) in size. | Creating huge sparse test devices; the "lower" of test stacks |
error |
All I/O returns -EIO. |
Fencing bad ranges; simulating broken devices in tests; "reject the tail of a resized LV" |
delay |
Injects configurable read/write latency before forwarding. | Testing I/O behaviour under slow storage; reproducing timeout bugs |
flakey |
Cycles between working and broken states on a timer. | Testing filesystem resilience to transient failures |
writecache |
Write-only persistent cache on a fast device (pmem/SSD) in front of a slow origin. | LVM writecache (lvconvert --type writecache) |
Note
They're all the same thing. LVM linear LV = dm-linear. LVM mirror = dm-raid. LUKS = dm-crypt. LVM thin = dm-thin-pool + dm-thin. LVM cache = dm-cache. multipathd = dm-multipath. veritysetup = dm-verity. The userspace tool's job is to decide the policy and write the table; the kernel does the I/O.
Three "Different" Features, One Substrate
lvcreate -L 10G vg0# Resulting dm table (abbreviated):
0 20971520 linear /dev/sda3 2048LVM's userspace lvm binary updates PV metadata, then calls DM_TABLE_LOAD + DM_DEV_SUSPEND/RESUME ioctls to install this table under /dev/mapper/vg0-mylv.
cryptsetup open /dev/sda3 cryptroot# Resulting dm table:
0 20971520 crypt aes-xts-plain64 \
:0:logon:cryptsetup:XXX 0 \
/dev/sda3 32768cryptsetup reads the LUKS header (slot keys, KDF, cipher), unlocks the master key, plants it in the kernel keyring, then installs a crypt table. The data offset (32768 sectors) skips past the LUKS header on disk.
lvcreate --thin vg0/pool0 -V 100G -n mythin# Two dm devices are created:
# (1) the pool itself
0 2097152 thin-pool /dev/mapper/vg0-pool0_tmeta /dev/mapper/vg0-pool0_tdata 128 0 1 skip_block_zeroing
# (2) a thin volume carved out of it (id = 1, virtual size 100 GiB)
0 209715200 thin /dev/mapper/vg0-pool0-tpool 1The thin volume is empty-allocated — blocks come from the pool only when written. Same kernel mechanism as LVM and LUKS, different target modules.
Tools & Interfaces
dmsetup — the low-level CLI
dmsetup ls— list all dm devicesdmsetup ls --tree— show the full stack (PV → LUKS → LV)dmsetup info <name>— state, open count, UUID, major/minordmsetup table <name>— dump the live tabledmsetup status <name>— target-specific runtime statusdmsetup create <name> --table <spec>— build a device manuallydmsetup suspend/resume— freeze I/O to rewrite the tabledmsetup reload <name>— load a new table (use beforeresume)dmsetup remove/remove_alldmsetup message— send target-specific commands (e.g. thin snapshot deletion)
sysfs & devtmpfs
/dev/dm-N— raw block device node (unstable across boots)/dev/mapper/<name>— friendly named symlink created by udev/sys/block/dm-N/dm/name— the mapper name/sys/block/dm-N/dm/uuid— stable UUID (e.g.LVM-xxx,CRYPT-LUKS2-xxx)/sys/block/dm-N/dm/suspended—0or1/sys/block/dm-N/holders/— dm devices stacked on top of this one/sys/block/dm-N/slaves/— devices this one sits on
dmstats (part of device-mapper package): per-region I/O accounting — reads, writes, merges, latencies, histograms. Useful when iostat's per-device view is too coarse.
Hands-on — Building a Linear dm Device Manually
Suppose you have two loop devices you want to concatenate into one 200 MiB virtual block device. No LVM, no filesystem — just dm.
- Set up two backing files:
console # fallocate -l 100M /tmp/part1.img # fallocate -l 100M /tmp/part2.img # losetup -f --show /tmp/part1.img /dev/loop0 # losetup -f --show /tmp/part2.img /dev/loop1 - Create the dm device with a two-row linear table. 100 MiB = 204800 sectors. The heredoc is fed as stdin to
dmsetup create:console # dmsetup create mymap <<EOF 0 204800 linear /dev/loop0 0 204800 204800 linear /dev/loop1 0 EOFThis creates
/dev/mapper/mymapand the underlying/dev/dm-N. - Inspect the result:
console # dmsetup ls mymap (253:0) # dmsetup info mymap Name: mymap State: ACTIVE Read Ahead: 256 Tables present: LIVE Open count: 0 Event number: 0 Major, minor: 253, 0 Number of targets: 2 # dmsetup table mymap 0 204800 linear 7:0 0 204800 204800 linear 7:1 0 # dmsetup status mymap 0 204800 linear 204800 204800 linear # lsblk /dev/mapper/mymap NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS mymap 253:0 0 200M 0 dm ├─loop0 7:0 0 100M 0 loop └─loop1 7:1 0 100M 0 loop - Put a filesystem on it and verify it spans both backing files:
console # mkfs.ext4 /dev/mapper/mymap # mount /dev/mapper/mymap /mnt # df -h /mnt Filesystem Size Used Avail Use% Mounted on /dev/mapper/mymap 189M 24K 175M 1% /mnt - Tear down:
console # umount /mnt # dmsetup remove mymap # losetup -d /dev/loop0 /dev/loop1
Tip
Pedagogical value: This is exactly what LVM does when you create a linear LV spanning two PVs — except LVM also writes VG metadata onto the PVs and tracks extent allocation. The kernel-side plumbing is identical.
dm in Action on a Real System
Here's what a typical encrypted-LVM laptop looks like. NVMe partition holds a LUKS container, LUKS-decrypted device is a PV, PV holds a VG with root/swap/home LVs:
# dmsetup ls --tree
vg0-home (253:4)
└─cryptroot (253:0)
└─ (259:3)
vg0-swap (253:3)
└─cryptroot (253:0)
└─ (259:3)
vg0-root (253:2)
└─cryptroot (253:0)
└─ (259:3)
vg0-pool0_tdata (253:6)
└─cryptroot (253:0)
└─ (259:3)
vg0-pool0_tmeta (253:5)
└─cryptroot (253:0)
└─ (259:3)
cryptroot (253:0)
└─ (259:3)
# lsblk
NAME MAJ:MIN SIZE TYPE MOUNTPOINTS
nvme0n1 259:0 1T disk
├─nvme0n1p1 259:1 1G part /boot/efi
├─nvme0n1p2 259:2 2G part /boot
└─nvme0n1p3 259:3 997G part
└─cryptroot 253:0 997G crypt
├─vg0-root 253:2 50G lvm /
├─vg0-swap 253:3 16G lvm [SWAP]
└─vg0-home 253:4 931G lvm /home
# ls -l /dev/mapper/
crw------- 1 root root 10, 236 control
lrwxrwxrwx 1 root root 7 cryptroot -> ../dm-0
lrwxrwxrwx 1 root root 7 vg0-root -> ../dm-2
lrwxrwxrwx 1 root root 7 vg0-swap -> ../dm-3
lrwxrwxrwx 1 root root 7 vg0-home -> ../dm-4
# dmsetup table cryptroot
0 2090860544 crypt aes-xts-plain64 :64:logon:cryptsetup:... 0 259:3 32768 1 allow_discards
# dmsetup table vg0-root
0 104857600 linear 253:0 2048
Note
Read this carefully. vg0-root is a linear map onto 253:0 (which is cryptroot). A write to / hits ext4 → dm-linear (vg0-root) → dm-crypt (cryptroot) → partition → NVMe. Every layer is a dm target. Every major-minor 253:X is just another row in someone's table.
Suspend / Resume — The Atomic Table Swap
Why it matters
dm devices can be suspended: the kernel stops dispatching new I/O to the target and queues incoming bios. While suspended, userspace can load a new table, and on resume the new mapping takes effect atomically. Queued bios are reissued under the new table.
This is the mechanism behind every online reconfiguration in the Linux block stack:
- LVM
pvmove— migrate extents off a PV without unmounting - LVM
lvresize— online grow/shrink of a logical volume - Thin pool resize — extend metadata or data devices under load
- LUKS online re-encryption (
cryptsetup reencrypt) — swap cipher/key while mounted - lvm-cache attach/detach — change caching policy or target without downtime
- Multipath failover — swap active paths on SAN-side failure
Manual flow
# dmsetup suspend mymap
# dmsetup reload mymap <<EOF
0 409600 linear /dev/loop2 0
EOF
# dmsetup resume mymapBetween suspend and resume, filesystems mounted on the device will block on any new I/O. Keep the window short — or use --noflush if the upper layer can tolerate it.
Filesystem freeze coordination
dm issues a filesystem freeze (FREEZE_SUPER) on suspend so in-flight transactions are flushed and journaled — then it's safe to swap the table. On resume, the filesystem is thawed. This is why lvcreate -s (snapshot) doesn't corrupt ext4/XFS.
udev & Event Propagation
From kernel event to /dev/mapper/<name>
- Kernel creates a new dm device. It's
/dev/dm-N— the name is an internal counter. - Kernel emits a uevent (
add) via netlink. systemd-udevdreceives it, matches against rules in/usr/lib/udev/rules.d/10-dm.rulesand13-dm-disk.rules.- udev reads
DM_NAME,DM_UUID,DM_SUSPENDEDfrom/sys/block/dm-N/dm/. - udev creates
/dev/mapper/<DM_NAME>as a symlink to/dev/dm-N. - udev runs
blkidto detect any filesystem/LUKS/LVM signatures; these populateby-uuid,by-label,by-partuuid. - If the device is a PV,
lvm2-pvscan@.serviceis triggered to activate dependent VGs. - If it's a LUKS header,
systemd-cryptsetup@.servicemay kick off decryption.
Warning
Beware: when troubleshooting, udevadm settle is your friend — many dm operations return before udev has finished creating symlinks. Scripts that call lvcreate then mkfs /dev/mapper/... immediately can race.
dm-integrity — Per-Block Checksumming
What it solves
A normal block device will happily return silently-corrupted data if the disk flips a bit (NAND wear, bad cable, buggy SATA link, firmware bug, cosmic ray). Filesystems like ext4 don't checksum data blocks. dm-integrity interposes a per-sector checksum and a journal so every read is verified.
Layout on disk
- The underlying device is carved into: superblock → journal → data + tag interleave
- Each data sector gets a tag — typically a CRC32C (4 bytes) or SHA256 (32 bytes) or HMAC-SHA256 (32 bytes)
- Journal absorbs writes atomically: the tag and data land together, or not at all
- Exposes a smaller "data" device to the upper layer (raw capacity minus metadata overhead)
Two modes
- Standalone (
integritysetup format): CRC32C catches random corruption. Not authenticated — an attacker who can write arbitrary blocks can forge a matching CRC. - Under dm-crypt (LUKS2
--integrity): HMAC-SHA256 tag, key derived from LUKS master key. Provides authenticated encryption (AEAD semantics) at the block layer. An attacker flipping ciphertext bits causes tag mismatch → EIO.
Layering for authenticated encryption
dm-crypt — encrypts plaintext → ciphertext, derives integrity keydm-integrity — HMACs ciphertext, stores tag + journalA bit-flip on disk → integrity tag mismatch on read → EIO surfaces to dm-crypt → surfaces to the filesystem. Silent corruption becomes loud corruption, which is always preferable.
Warning
Cost: journaling means every write hits the disk twice (journal + final location). Enable --integrity-no-journal for bitmap mode if you only need corruption detection and can tolerate a potentially inconsistent tag on crash. For write-heavy workloads, the performance hit is 30–50%.
dm-verity — Merkle-Tree Verified Read-Only
The idea
dm-verity is the read-only counterpart to dm-integrity. It hashes every data block, builds a Merkle tree over those hashes, and requires a signed root hash to activate the device. Any block whose hash doesn't chain up to the root hash returns EIO on read.
This proves, at boot time, that the root filesystem is exactly what the vendor signed — no on-disk tampering possible without either breaking the hash function or forging the signing key.
Structure
- Data device — the read-only filesystem image (ext4, squashfs, erofs).
- Hash device — either a separate device or a region at the end of the data device. Stores the hash tree: leaves = hashes of 4 KiB data blocks, inner nodes = hashes of child hashes.
- Root hash — 32 bytes (SHA256). Passed to
dm-verityat activation. - Salt — per-image random salt to prevent rainbow-table attacks.
- Signature — the root hash is normally signed by a bootloader-verified key (UEFI Secure Boot / Android AVB / ChromeOS verified boot).
Verify path (read)
Read data block
from data device
Hash it
SHA256(salt ‖ block)
Compare to leaf
in hash tree
Walk up tree
to root
Match root hash?
yes → return data; no → EIO
Note
Real-world users: Android (every system and vendor partition since Android 10), ChromeOS (root FS), confidential-computing container images, Fedora Silverblue / Bootc, Azure confidential VMs. Anywhere "I need to prove the OS hasn't been tampered with" matters.
mdadm vs dm-raid — One Engine, Two Front-Ends
A common source of confusion: Linux has two ways to run software RAID, and people aren't sure which to use. They share a kernel engine.
- Standalone RAID layer — the oldest Linux RAID
- User tool:
mdadm - Devices:
/dev/md0,/dev/md1… - Metadata formats:
0.9,1.0,1.1,1.2(1.2is default) - Typical use: build a RAID array, put LVM on top of it
- Kernel code:
drivers/md/md.c+raid*.c
- Wraps the same kernel MD code into a dm target
- User tool:
lvcreate --type raid5etc. - Devices:
/dev/mapper/vg0-myraidlv - Metadata: stored in LVM's VG metadata — no separate RAID superblock
- Typical use: integrated RAID + LVM management with one tool
- Kernel code:
drivers/md/dm-raid.c(thin wrapper) + the sameraid*.c
Tip
Rule of thumb: if you already use LVM, lvcreate --type raid* keeps everything in one place. If you run bare RAID (ZFS/btrfs-style arrays without LVM) or need features LVM doesn't expose (e.g. RAID-0 with specific chunk sizes, write-mostly members), use mdadm. Either way, the rebuild/resync code running is the same. (Covered in depth in storage-03.)
Mental Model — The Collapse
Checklist of things you now understand for free
- Why
lsblkshowscrypt,lvm,raid5all as TYPEdmunderneath — they're all dm targets - Why
/dev/mapper/contains entries from several distinct tools - Why
pvmoveis online — it's a table swap under suspend - Why LUKS2 can do authenticated encryption —
dm-cryptstacks ondm-integrity - Why Android's verified boot looks like "magic" — it's
dm-veritywith a signed root hash - Why Docker's old
devicemapperstorage driver existed —dm-thinCoW snapshots per container - Why LVM and
mdadmcan both do RAID — they share the MD kernel engine - Why a single
dmsetup ls --treecommand visualizes your entire stack
Tip
Next: storage-05 covers LVM (the userspace controller that drives dm-linear, dm-stripe, dm-raid, and dm-thin-pool). storage-06 covers LUKS & dm-crypt in depth. Both build on the dm mental model established here.