Skip to content
Menu

Linux Storage16 min read

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*.c handles 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:

console
# 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

console
# 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 0

This 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.

Filesystem — ext4 (journaled write, 4 KiB block)
dm-linear — LV "root" — translates LV sector → VG extent sector
dm-crypt — AES-XTS encrypts each sector with a per-sector tweak
Partition — /dev/nvme0n1p3 (offset + length on disk)
Block device — NVMe controller + NAND

What actually happens on a single write

  1. ext4 issues a bio (block I/O request) for logical sector Sfs on /dev/mapper/vg0-root.
  2. Kernel sees the target device is a dm device; the request enters dm.c.
  3. dm-linear looks up Sfs in its table, picks the matching row, rewrites the bio to target /dev/mapper/cryptroot at sector Slv, forwards it.
  4. dm-crypt receives the bio, allocates a bounce page, encrypts each 512-byte sector with AES-XTS using Slv as the tweak, retargets the bio to /dev/nvme0n1p3 at Slv + offset, submits it to a workqueue (encryption is async).
  5. The partition layer adds the partition start offset, hands off to the NVMe driver.
  6. On completion, each dm target's endio fires 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.

TargetWhat it doesUsed 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

LVM — lvcreate -L 10G vg0
console
# Resulting dm table (abbreviated):
0 20971520 linear /dev/sda3 2048

LVM'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.

LUKS — cryptsetup open /dev/sda3 cryptroot
console
# Resulting dm table:
0 20971520 crypt aes-xts-plain64 \
  :0:logon:cryptsetup:XXX 0 \
  /dev/sda3 32768

cryptsetup 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.

Thin provisioning — lvcreate --thin vg0/pool0 -V 100G -n mythin
console
# 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 1

The 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 devices
  • dmsetup ls --tree — show the full stack (PV → LUKS → LV)
  • dmsetup info <name> — state, open count, UUID, major/minor
  • dmsetup table <name> — dump the live table
  • dmsetup status <name> — target-specific runtime status
  • dmsetup create <name> --table <spec> — build a device manually
  • dmsetup suspend / resume — freeze I/O to rewrite the table
  • dmsetup reload <name> — load a new table (use before resume)
  • dmsetup remove / remove_all
  • dmsetup 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/suspended0 or 1
  • /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.

  1. 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
  2. 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
    EOF

    This creates /dev/mapper/mymap and the underlying /dev/dm-N.

  3. 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
  4. 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
  5. 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:

console
console

# 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

console
# dmsetup suspend mymap
# dmsetup reload mymap <<EOF
0 409600 linear /dev/loop2 0
EOF
# dmsetup resume mymap

Between 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>

  1. Kernel creates a new dm device. It's /dev/dm-N — the name is an internal counter.
  2. Kernel emits a uevent (add) via netlink.
  3. systemd-udevd receives it, matches against rules in /usr/lib/udev/rules.d/10-dm.rules and 13-dm-disk.rules.
  4. udev reads DM_NAME, DM_UUID, DM_SUSPENDED from /sys/block/dm-N/dm/.
  5. udev creates /dev/mapper/<DM_NAME> as a symlink to /dev/dm-N.
  6. udev runs blkid to detect any filesystem/LUKS/LVM signatures; these populate by-uuid, by-label, by-partuuid.
  7. If the device is a PV, lvm2-pvscan@.service is triggered to activate dependent VGs.
  8. If it's a LUKS header, systemd-cryptsetup@.service may 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: superblockjournaldata + 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

Filesystem (ext4 / xfs / btrfs)
dm-crypt — encrypts plaintext → ciphertext, derives integrity key
dm-integrity — HMACs ciphertext, stores tag + journal
Partition
Disk

A 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-verity at 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)

  1. Read data block

    from data device

  2. Hash it

    SHA256(salt ‖ block)

  3. Compare to leaf

    in hash tree

  4. Walk up tree

    to root

  5. 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.

mdadm / md
  • 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.2 is default)
  • Typical use: build a RAID array, put LVM on top of it
  • Kernel code: drivers/md/md.c + raid*.c
dm-raid (via LVM)
  • Wraps the same kernel MD code into a dm target
  • User tool: lvcreate --type raid5 etc.
  • 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 same raid*.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

Before
LVM is a volume manager. LUKS is an encryption layer. Thin provisioning is a storage-allocation feature. Multipath is a SAN thing. Verity is an Android thing. They feel unrelated.
After
All of them are userspace controllers that build dm tables and install them in the kernel. The differences are policy and metadata — the I/O plumbing is one framework.

Checklist of things you now understand for free

  • Why lsblk shows crypt, lvm, raid5 all as TYPE dm underneath — they're all dm targets
  • Why /dev/mapper/ contains entries from several distinct tools
  • Why pvmove is online — it's a table swap under suspend
  • Why LUKS2 can do authenticated encryption — dm-crypt stacks on dm-integrity
  • Why Android's verified boot looks like "magic" — it's dm-verity with a signed root hash
  • Why Docker's old devicemapper storage driver existed — dm-thin CoW snapshots per container
  • Why LVM and mdadm can both do RAID — they share the MD kernel engine
  • Why a single dmsetup ls --tree command 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.

Solidnines — solidnines.com