Technical Root Cause Analysis
1. Components and Architecture
- Source File:
drivers/md/dm-integrity.c
- Structures and Fields:
struct dm_integrity_c:
journal_sections: Total number of journal sections configured on the device.
journal_section_entries: Number of journal entries per section.
journal_entries: Total entries across all sections (journal_sections * journal_section_entries).
n_uncommitted_sections: Number of journal sections containing uncommitted entries.
n_committed_sections: Number of committed journal sections awaiting writeback to data areas.
free_sectors: Number of free entries/slots currently available in the journal.
free_section, free_section_entry, uncommitted_section, committed_section.
mode: Target operating mode ('J' for Journal, 'B' for Bitmap, 'D' for Direct, 'R' for Recovery, 'I' for Inline).
- Key Functions:
dm_integrity_ctr()
dm_integrity_resume()
replay_journal()
integrity_commit()
pad_uncommitted()
dm_integrity_postsuspend() / submit_flush_bio()
2. Defect Mechanism and Lifecycle Invariant Violation
In drivers/md/dm-integrity.c, the journal state machine maintains an accounting invariant across all journal entries:
$$\text{journal_sections} \times \text{journal_section_entries} = (n_uncommitted_sections + n_committed_sections) \times \text{journal_section_entries} + free_sectors$$
During target construction in dm_integrity_ctr(), struct dm_integrity_c *ic is allocated with kzalloc_obj(), leaving all fields zeroed. calculate_device_limits() calculates ic->journal_sections, ic->journal_section_entries, and ic->journal_entries. However, the journal position and capacity accounting fields (free_sectors, free_section, uncommitted_section, committed_section, n_committed_sections, n_uncommitted_sections) are not initialized in dm_integrity_ctr(). Instead, their initialization is deferred to replay_journal():
static void replay_journal(struct dm_integrity_c *ic)
{
...
ic->committed_section = continue_section;
ic->n_committed_sections = 0;
ic->uncommitted_section = continue_section;
ic->n_uncommitted_sections = 0;
ic->free_section = continue_section;
ic->free_section_entry = 0;
ic->free_sectors = ic->journal_entries;
...
}
When the device mapper target is resumed via dm_integrity_resume(), the driver re-reads the superblock from the underlying device with sync_rw_sb(ic, REQ_OP_READ) and checks whether any immutable geometry parameters were changed behind its back via superblock_changed(ic):
if (unlikely(superblock_changed(ic))) {
memcpy(ic->sb, ic->sb_copy, sizeof(struct superblock));
DMERR("The superblock was changed while the device was suspended");
dm_integrity_io_error(ic, "superblock check", -EINVAL);
goto skip_writes;
}
If superblock_changed(ic) returns true (e.g., if the backing block device was modified between target creation and resume):
dm_integrity_io_error() records the error in ic->failed.
- Execution jumps directly to
skip_writes:, which bypasses replay_journal(ic).
- Consequently,
ic->free_sectors, ic->n_uncommitted_sections, and ic->n_committed_sections are never initialized and remain 0.
3. How the Failure Manifests
Even though the device is marked as failed, work items can still be queued to ic->commit_wq:
When the worker thread executes integrity_commit() on ic->commit_wq:
- For journal mode (
ic->mode == 'J'), it calls pad_uncommitted(ic).
pad_uncommitted() evaluates the journal entry invariant:
if (WARN_ON(ic->journal_sections * ic->journal_section_entries !=
(ic->n_uncommitted_sections + ic->n_committed_sections) *
ic->journal_section_entries + ic->free_sectors))
- Because
replay_journal() was skipped, the right-hand side evaluates to $(0 + 0) \times \text{journal_section_entries} + 0 = 0$, whereas the left-hand side is positive ($\text{journal_sections} \times \text{journal_section_entries} > 0$).
- The invariant check fails, triggering the reported
WARN_ON at drivers/md/dm-integrity.c:2885.