Skip to content

vdev_mirror: avoid false scrub errors during sequential rebuilds - #19248

Closed
Zaczero wants to merge 2 commits into
openzfs:masterfrom
Zaczero:fix/scrub-rebuild-overlap
Closed

Zaczero wants to merge 2 commits into
openzfs:masterfrom
Zaczero:fix/scrub-rebuild-overlap

Conversation

@Zaczero

@Zaczero Zaczero commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Motivation and Context

A sequential attach or replacement publishes its new child before the rebuild thread cancels an active scrub. Scrub reads can reach that child before its data has been copied and report checksum errors against a healthy device. This affects mirrors and replacing or spare columns beneath dRAID, including successive replacements overlapping the post-rebuild scrub.

Waiting for the scrub to finish before starting another sequential attach or replacement avoids the overlap.

Description

Keep the scrub's normal reads of intact children, but skip rebuilding children while the sequential rebuild is active. Snapshot that state in the mirror map so dispatch and completion agree even if the worker finishes between them. Persistent leaf rebuild markers can outlive the worker while missing-data DTLs remain; they must not keep children excluded from the subsequent scrub.

Preserve last-resort reads from DTL-marked children: the DTL covers a transaction range, and an individual block may already have been copied. A nested column read without a block pointer cannot authorize repairs from those bytes. Leave repairs to the rebuild worker or checksum-owning parent.

A preparatory commit makes scrub, resilver and replacement waits fail on timeout, and makes verify_pool require a completed scrub with zero errors.

How Has This Been Tested?

The tests passed in an isolated Linux debug guest:

  • scrub_rebuild covers ordinary mirror attachment and a dRAID replacement beneath an established distributed spare. It checks scrub cancellation, a clean subsequent scrub and data after export/import.
  • scrub_rebuild_residual leaves failed writes on an online replacement after worker completion. It requires the first scrub to finish with zero errors, then verifies the data after export/import with another child offline and no intervening healing pass. Three data columns ensure that the changed pattern also changes parity.
  • ztest holds a real rebuild and submits scrub reads through the normal I/O pipeline. It varies dRAID parity, data width and sector size. With source read failures exhausting parity, recovery requires a valid but DTL-marked replacement copy. A corrupted candidate must be rejected without overwriting the original; parent-verified repairs remain possible. The scenario runs during initialization, before the concurrent workload, so its scan suspension and fault injection remain isolated.

Negative controls check the need for active-rebuild state, last-resort reads and raw-read repair suppression.

Types of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Performance enhancement (non-breaking change which improves efficiency)
  • Code cleanup (non-breaking change which makes code smaller or more readable)
  • Quality assurance (non-breaking change which makes the code more robust against bugs)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Library ABI change (libzfs, libzfs_core, libnvpair and libzfsbootenv)
  • Documentation (a change to man pages or other documentation)

Checklist

@Zaczero Zaczero changed the title Avoid false checksum errors from scrubs during sequential rebuilds vdev_mirror: avoid false scrub errors during sequential rebuilds Oct 4, 2026
@Zaczero
Zaczero force-pushed the fix/scrub-rebuild-overlap branch 14 times, most recently from 3ff3e98 to dcebc54 Compare October 7, 2026 14:38
@Zaczero
Zaczero marked this pull request as ready for review October 7, 2026 14:40
@github-actions github-actions Bot added Status: Code Review Needed Ready for review and testing and removed Status: Work in Progress labels Oct 7, 2026
Comment thread module/zfs/vdev_mirror.c
* Leaf markers can outlive the rebuild worker while DTLs
* remain.
*/
mm->mm_rebuild_active = vd->vdev_top->vdev_rebuilding;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need to add another field for this. The mm->mm_rebuilding field already indicates if there's an active rebuild, and each mirror child stores a similar mc->mc_rebuilding boolean. What we want is for the scrub IO selection logic to skip any children which are known to be rebuilding, while still reading from all online and intact children. This is untested but I believe should do the trick:

diff --git a/module/zfs/vdev_mirror.c b/module/zfs/vdev_mirror.c
index 844889af1e..83cb725e67 100644
--- a/module/zfs/vdev_mirror.c
+++ b/module/zfs/vdev_mirror.c
@@ -634,6 +634,14 @@ vdev_mirror_io_start(zio_t *zio)
                                        continue;
                                }
 
+                               /* Don't issue ZIOs to rebuilding children */
+                               if (mc->mc_rebuilding) {
+                                       mc->mc_error = SET_ERROR(ESTALE);
+                                       mc->mc_skipped = 1;
+                                       mc->mc_speculative = 1;
+                                       continue;
+                               }
+
                                mc->mc_abd = first ? zio->io_abd :
                                    abd_alloc_sametype(zio->io_abd,
                                    zio->io_size);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use your suggested child-skipping approach, keeping scrub reads of intact children.

I retained the active-rebuild snapshot because mm_rebuilding tracks leaf markers that can remain set after the worker finishes. Skipping based on those markers alone also skips the replacement during the subsequent scrub. The residual-data test reproduces this: the scrub repairs nothing, and importing with another child offline then fails.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, looking at this again I believe there's a better approach. The core issue is the small overlap between when a rebuild thread completes and the scrub starts. Rather than modify the mirror code to tolerate this; we should do the opposite. That is, 1) update the scrub code to defer the handful of txgs until the rebuild thread exits to prevent the overlap, and 2) ASSERT0P(vd->vdev_top->vdev_rebuilding) in the mirror scrub path.

The point of automatically kicking off the scrub after a sequential rebuild is to verify the data on all reconstructed disks using the block checksums. If we skip reading online children during the scrub, then there's the risk that the scrub completes and fails to detect a block which was for some reason incorrectly reconstructed from parity.

We already skip issuing scrubs for a TXGs for variety of reasons, the code is built to allow this, so a small change like this should be sufficient:

diff --git a/module/zfs/dsl_scan.c b/module/zfs/dsl_scan.c
index 2d00faa78f..b6a3b6d968 100644
--- a/module/zfs/dsl_scan.c
+++ b/module/zfs/dsl_scan.c
@@ -4580,6 +4580,10 @@ dsl_scan_sync(dsl_pool_t *dp, dmu_tx_t *tx)
        if (!scn->scn_async_stalled && !dsl_scan_active(scn))
                return;
 
+       /* If a rebuild thread is still active, wait until it's done */
+       if (vdev_rebuild_active(spa->spa_root_vdev))
+               return;
+
        /* reset scan statistics */
        scn->scn_visited_this_txg = 0;
        scn->scn_async_frees_this_txg = 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continued in #19432

Comment thread module/zfs/vdev_mirror.c Outdated
mm->mm_rebuild_active)
return;

if (good_copies && spa_writeable(zio->io_spa) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then this shouldn't be needed since mm->mm_resilvering won't be set.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unexpected_errors can still trigger repairs when mm_resilvering is false. If the original read fails and we fall back to the replacement, a nested column read has no block pointer with which to verify those bytes.

Removing this guard makes the recovery test overwrite the original with corrupt replacement data before the parent rejects the checksum. I’ve therefore kept it; parent-verified repairs still work.

Comment thread tests/zfs-tests/tests/functional/replacement/scrub_rebuild_recovery.ksh Outdated
wait_scrubbed(), wait_resilvered() and wait_replacing() return the
status of their last command. After a timeout that is the final sleep,
so log_must accepts a wait that never saw the pool reach its state.
Return failure after the timeout instead.

verify_pool() also accepted a scrub that finished with errors, as long
as no checksum counter was set. Require the completed scrub to report
zero errors.

Signed-off-by: Kamil Monicz <kamil@monicz.dev>
A sequential attach or replacement makes its new child visible before
the rebuild thread cancels an active scrub. Scrub reads can reach that
child before its data has been copied and report checksum errors on a
healthy device. Replacing an original beneath an established dRAID spare
can overlap the scrub this way, too.

Keep scrub reads of intact children, but skip rebuilding children while
the rebuild is active. Snapshot that runtime state so dispatch and
completion agree. Persistent leaf rebuild markers can outlive the worker
and must not exclude children from the subsequent scrub.

For nested column reads without a block pointer, defer repairs to the
rebuild worker or checksum-owning parent. Keep last-resort reads from
DTL-marked children: individual blocks may already have been copied and
can still be needed for recovery.

Cover ordinary and nested dRAID overlaps in ZTS. Also require the first
post-rebuild scrub to repair residual missing data before verifying the
replacement with another child offline.

Exercise recovery in ztest with varying dRAID parity, data width and
sector size. A valid replacement copy must permit recovery when source
failures exhaust parity. A corrupted candidate must be rejected without
overwriting the original, while parent-verified repairs remain possible.

Signed-off-by: Kamil Monicz <kamil@monicz.dev>
@Zaczero
Zaczero force-pushed the fix/scrub-rebuild-overlap branch from dcebc54 to a7bab2c Compare October 15, 2026 10:25
Comment thread cmd/ztest.c
nvlist_t *root = fnvlist_alloc();
fnvlist_add_string(root, ZPOOL_CONFIG_TYPE, VDEV_TYPE_ROOT);
fnvlist_add_nvlist_array(root, ZPOOL_CONFIG_CHILDREN,
(const nvlist_t **)&draid, 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case of a huge ztest test, which make me shiver about its maintenance later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this looks unmaintainable. It might be a bit tricky, but surely we could setup this scenario as a ZTS test.

@Zaczero Zaczero Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still trying to figure out the testing side of things on ZFS; sorry for that. I will take a deeper look. Because of testing, I still have many of my PRs as drafts (or unpublished) and needing work, so it's not that I'm ignoring the problem!

@Zaczero

Zaczero commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #19432

@Zaczero Zaczero closed this Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Code Review Needed Ready for review and testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants