Device Disappearance States
When a physical drive disappears from a btrfs pool (hot-unplug, cable failure, drive death), the system passes through several states depending on how far the failure has progressed and whether the LUKS mapper is still open. Each state produces different output from btrfs filesystem show, btrfs device stats, and cryptsetup status — and braid must handle each combination correctly.
This mapping is not derivable from reading braid’s code or btrfs docs alone — it requires cross-tool knowledge that’s easy to get wrong.
State Table
| State | btrfs filesystem show | btrfs device stats | cryptsetup status | braid maps to |
|---|---|---|---|---|
| Healthy | path /dev/mapper/X | [/dev/mapper/X] | device: /dev/sdY | pool.devices |
| Null-underlying | path /dev/mapper/X | [/dev/mapper/X] | device: (null) | pool.null_underlying |
| MISSING with path | path /dev/mapper/X MISSING | [/dev/mapper/X] (??) | not queried | missing_devids only |
| Fully gone | path MISSING | [devid:N] | not queried | missing_devids |
Empirical note: SATA hot-unplug on real hardware enters Null-underlying immediately and stays there for at least 5 minutes without I/O pressure. We have not yet observed the MISSING-with-path state in practice. See real-world/sata-hot-unplug.md for full test results.
Healthy
Normal operation. Physical drive is present, LUKS mapper is open and points to the underlying block device, btrfs sees the device.
Null-underlying
Hot-unplug while mounted. The LUKS mapper (/dev/mapper/braid-X) is still open in device-mapper, but the backing block device has vanished. cryptsetup status reports device: (null). btrfs still sees the mapper path — it doesn’t know the physical drive is gone until I/O fails.
braid handles this correctly: probe_pool detects the (null) device and records it in pool.null_underlying. monitor’s own probe, probe_pool_alerts, records only its devid (null_underlying_devids), and monitor includes that devid in the alert-local missing set (AlertDevids.missing). The stats row reports both the mapper path and the devid; the alert pipeline pairs by devid directly. braid doctor’s pool_missing_devices check also keys on missing_count (which counts null-underlying members), so it warns here rather than reporting a green “no missing devices” — and routes the operator to the re-seat or promote-then-replace recovery (braid replace and remove-missing refuse a null-underlying devid until btrfs promotes it to MISSING).
btrfs also keeps the member writable. btrfs device usage still prints its mapper path and full size: btrfs-progs reads the size with a BLKGETSIZE64 ioctl on the mapper, which returns the dm table’s size without touching the backing disk (btrfs-progs v6.19.1, common/device-utils.c (device_get_partition_size), called from cmds/filesystem-usage.c (load_device_info)). The kernel chunk allocator still places new chunk copies on it. So remove-missing refuses every target while any other member is in this state (cli/src/types.rs#PoolState::hot_unplugged_not_missing); relocating chunks would put copies on a disk that is gone. See ADR 012.
Post-UUID-identity rule: when a mapper is null-underlying, the backing disk’s
LUKS header is gone, but the mapper’s dm UUID (/sys/block/dm-N/dm/uuid) still
records the LUKS UUID cryptsetup opened it with. braid binds the live mapper back
to membership by that UUID (cli/src/dm_identity.rs#mapper_luks_uuid), as it
does for a mapper whose disk is present. So status’s compact Drives: row,
replace, and every recovery path take a null-underlying member’s devid from
its mapper, not from pool.json, which may hold none or a stale one. Recovery
does this through one binding pass (cli/src/membership.rs#bind_live_pool)
and keeps the member in pool.json. Persisted
DiskMember.devid binds only a device btrfs reports MISSING with no open
mapper (cli/src/types.rs#PoolState::devid_only_missing); it is prior-binding
state, not display authority, and status output still uses live btrfs stats
for displayed devids.
MISSING with path
btrfs has registered the device as missing, but still remembers which mapper path it had. btrfs filesystem show appends MISSING to the path. The parser puts the devid into missing_devids but discards the path. probe_pool never processes this device (it only iterates show.devices), so it doesn’t appear in pool.devices or pool.null_underlying.
Handling: btrfs device stats rows always carry a mandatory devid field, so the alert pipeline identifies the row by devid regardless of which path string btrfs reports ([/dev/mapper/X] or [devid:N]). The MissingDevice alert is generated independently from missing_devids. Rows for alert-local missing devids are skipped for BtrfsDeviceErrors, so the monitor latches no counts for them and braid ack commits none. If the member returns, the next monitor cycle alerts once on the counts it logged, including the I/O that failed as it went, and the next ack commits them (see ADR 014).
This is the one state where membership correlation falls back to devid: btrfs
reports the missing device only by devid, so braid resolves the member whose
persisted DiskMember.devid matches. It must not infer membership by parsing a
mapper name or LUKS label.
Uncertainty: We haven’t empirically confirmed which path string btrfs device stats reports for a device in this state – the ?? in the table marks this. The answer no longer affects correctness (devid drives the lookup), but it would still be useful empirical data.
Fully gone
Device is completely absent — either the LUKS mapper was torn down, or the device was missing at mount time (degraded mount). btrfs filesystem show reports bare path MISSING (no mapper path). The pinned btrfs-progs renders the missing-device stats path as [devid:N] (cmds/device.c#print_device_stat_string); [<missing disk>] is an older btrfs rendering. braid does not depend on either string: the parser ignores the device field and keeps the row’s devid and counters.
At this point there is no mapper and no observable LUKS UUID. Mutating commands
that target the missing device, such as remove-missing and missing-path
replace, resolve the requested btrfs devid through UUID-keyed membership and
fail closed if no persisted member carries that devid.
Transitions
The typical progression for a hot-unplug:
Healthy → Null-underlying → MISSING with path(?) → Fully gone
The transitions depend on timing, I/O activity, and whether the kernel tears down the LUKS mapper. A brief unplug-replug might only reach Null-underlying before recovering. A permanent removal eventually reaches Fully gone.
The transition from Null-underlying to MISSING with path is the least understood. It likely happens when btrfs attempts I/O on the device and gets errors, then marks it missing — but the mapper path is still in kernel memory so btrfs remembers it.
Code Pointers
probe_pool:cli/src/probe.rs– buildspool.devices,pool.null_underlying,pool.missing_devidsbtrfs filesystem showparser:cli/src/parse/btrfs_filesystem_show.rs– filters MISSING devices fromdeviceslistbtrfs device statsparser:cli/src/parse/btrfs_device_stats.rs– propagatesdevidas the btrfs-native stats row key and ignores the display-only device string- alert computation:
cli/src/alert.rs–compute_alert_statekeys bydev.devidfrom the parsed stats row and skips alert-local missing devids forBtrfsDeviceErrors;commit_latched_evidencewrites ack baselines by the latched cause’s devid