Skip to content

Commit 44c9ad2

Browse files
committed
added module param to optionally perform a thorough scrub
1 parent 8302b6e commit 44c9ad2

6 files changed

Lines changed: 187 additions & 8 deletions

File tree

man/man4/zfs.4

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.\" Copyright (c) 2019, 2021 by Delphix. All rights reserved.
55
.\" Copyright (c) 2019 Datto Inc.
66
.\" Copyright (c) 2023, 2024, 2025, Klara, Inc.
7+
.\" Copyright (c) 2026 ConnectWise, Inc.
78
.\" The contents of this file are subject to the terms of the Common Development
89
.\" and Distribution License (the "License"). You may not use this file except
910
.\" in compliance with the License. You can obtain a copy of the license at
@@ -17,7 +18,7 @@
1718
.\" own identifying information:
1819
.\" Portions Copyright [yyyy] [name of copyright owner]
1920
.\"
20-
.Dd May 29, 2025
21+
.Dd Jan 23, 2026
2122
.Dt ZFS 4
2223
.Os
2324
.
@@ -2087,7 +2088,15 @@ working on a scrub between TXG flushes.
20872088
.It Sy zfs_scrub_error_blocks_per_txg Ns = Ns Sy 4096 Pq uint
20882089
Error blocks to be scrubbed in one txg.
20892090
.
2090-
.It Sy zfs_scan_checkpoint_intval Ns = Ns Sy 7200 Ns s Po 2 hour Pc Pq uint
2091+
.It Sy zfs_scrub_thorough Ns = Ns Sy 0 Ns | Ns 1 Pq uint
2092+
When set will cause scrub to decrypt and decompress blocks it reads so that it
2093+
will catch the rare type of corruption where the checksum matches the data, but
2094+
decryption and/or decompression fails. For encrypted datasets keys have to be
2095+
have to be loaded in order to perform thorough scrub. If keys are not loaded
2096+
or unloaded during a thorough scrub the scrub will revert to doing a normal
2097+
scrub for the encrypted dataset.
2098+
.
2099+
.It Sy zfs_scan_checkpoint_intval Ns = Ns Sy 7200 Ns s Po 2 hours Pc Pq uint
20912100
To preserve progress across reboots, the sequential scan algorithm periodically
20922101
needs to stop metadata scanning and issue all the verification I/O to disk.
20932102
The frequency of this flushing is determined by this tunable.

module/zfs/dsl_scan.c

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@
2626
* Copyright (c) 2017, 2019, Datto Inc. All rights reserved.
2727
* Copyright (c) 2015, Nexenta Systems, Inc. All rights reserved.
2828
* Copyright 2019 Joyent, Inc.
29+
* Copyright 2025 ConnectWise, Inc.
2930
*/
3031

3132
#include <sys/dsl_scan.h>
3233
#include <sys/dsl_pool.h>
3334
#include <sys/dsl_dataset.h>
3435
#include <sys/dsl_prop.h>
3536
#include <sys/dsl_dir.h>
37+
#include <sys/dsl_crypt.h>
3638
#include <sys/dsl_synctask.h>
3739
#include <sys/dnode.h>
3840
#include <sys/dmu_tx.h>
@@ -56,6 +58,7 @@
5658
#include <sys/abd.h>
5759
#include <sys/range_tree.h>
5860
#include <sys/dbuf.h>
61+
#include <sys/fm/fs/zfs.h>
5962
#ifdef _KERNEL
6063
#include <sys/zfs_vfsops.h>
6164
#endif
@@ -246,6 +249,16 @@ static int zfs_free_bpobj_enabled = 1;
246249
/* Error blocks to be scrubbed in one txg. */
247250
static uint_t zfs_scrub_error_blocks_per_txg = 1 << 12;
248251

252+
/*
253+
* When set will cause scrub to decrypt and decompress blocks it reads so that
254+
* it will catch the rare type of corruption where the checksum matches the
255+
* data, but decryption and/or decompression fails. For encrypted datasets keys
256+
* have to be loaded in order to perform thorough scrub. If keys are not loaded
257+
* or unloaded during a thorough scrub the scrub will revert to doing a normal
258+
* scrub for the encrypted dataset.
259+
*/
260+
static uint_t zfs_scrub_thorough = 0;
261+
249262
/* the order has to match pool_scan_type */
250263
static scan_cb_t *scan_funcs[POOL_SCAN_FUNCS] = {
251264
NULL,
@@ -287,6 +300,10 @@ typedef struct scan_io {
287300
uint64_t sio_birth;
288301
zio_cksum_t sio_cksum;
289302
uint32_t sio_nr_dvas;
303+
uint64_t sio_salt;
304+
uint64_t sio_iv1;
305+
uint64_t sio_iv2;
306+
290307

291308
/* fields from zio_t */
292309
uint32_t sio_flags;
@@ -442,6 +459,11 @@ sio2bp(const scan_io_t *sio, blkptr_t *bp)
442459
BP_SET_PHYSICAL_BIRTH(bp, sio->sio_phys_birth);
443460
BP_SET_LOGICAL_BIRTH(bp, sio->sio_birth);
444461
bp->blk_fill = 1; /* we always only work with data pointers */
462+
if (BP_IS_ENCRYPTED(bp)) {
463+
bp->blk_dva[2].dva_word[0] = sio->sio_salt;
464+
bp->blk_dva[2].dva_word[1] = sio->sio_iv1;
465+
BP_SET_IV2(bp, sio->sio_iv2);
466+
}
445467
bp->blk_cksum = sio->sio_cksum;
446468

447469
ASSERT3U(sio->sio_nr_dvas, >, 0);
@@ -458,6 +480,11 @@ bp2sio(const blkptr_t *bp, scan_io_t *sio, int dva_i)
458480
sio->sio_birth = BP_GET_LOGICAL_BIRTH(bp);
459481
sio->sio_cksum = bp->blk_cksum;
460482
sio->sio_nr_dvas = BP_GET_NDVAS(bp);
483+
if (BP_IS_ENCRYPTED(bp)) {
484+
sio->sio_salt = bp->blk_dva[2].dva_word[0];
485+
sio->sio_iv1 = bp->blk_dva[2].dva_word[1];
486+
sio->sio_iv2 = BP_GET_IV2(bp);
487+
}
461488

462489
/*
463490
* Copy the DVAs to the sio. We need all copies of the block so
@@ -4013,8 +4040,11 @@ read_by_block_level(dsl_scan_t *scn, zbookmark_phys_t zb)
40134040
return;
40144041
}
40154042

4016-
int zio_flags = ZIO_FLAG_SCAN_THREAD | ZIO_FLAG_RAW |
4017-
ZIO_FLAG_CANFAIL | ZIO_FLAG_SCRUB;
4043+
int zio_flags = ZIO_FLAG_SCAN_THREAD | ZIO_FLAG_CANFAIL |
4044+
ZIO_FLAG_SCRUB;
4045+
4046+
if (zfs_scrub_thorough == 0)
4047+
zio_flags |= ZIO_FLAG_RAW;
40184048

40194049
/* If it's an intent log block, failure is expected. */
40204050
if (zb.zb_level == ZB_ZIL_LEVEL)
@@ -4814,7 +4844,24 @@ dsl_scan_scrub_cb(dsl_pool_t *dp,
48144844
uint64_t phys_birth = BP_GET_PHYSICAL_BIRTH(bp);
48154845
size_t psize = BP_GET_PSIZE(bp);
48164846
boolean_t needs_io = B_FALSE;
4817-
int zio_flags = ZIO_FLAG_SCAN_THREAD | ZIO_FLAG_RAW | ZIO_FLAG_CANFAIL;
4847+
int zio_flags = ZIO_FLAG_SCAN_THREAD | ZIO_FLAG_CANFAIL;
4848+
4849+
if (zfs_scrub_thorough == 0) {
4850+
zio_flags |= ZIO_FLAG_RAW;
4851+
} else if (BP_IS_ENCRYPTED(bp)) {
4852+
/*
4853+
* If we are doing a thorough scrub, the key may be unloaded
4854+
* at any time. So we fall back to a raw scrub if we don't have
4855+
* the key loaded.
4856+
*/
4857+
dsl_crypto_key_t *dck = NULL;
4858+
int err = spa_keystore_lookup_key(spa, zb->zb_objset, FTAG, &dck);
4859+
if (err == 0) {
4860+
spa_keystore_dsl_key_rele(spa, dck, FTAG);
4861+
} else {
4862+
zio_flags |= ZIO_FLAG_RAW;
4863+
}
4864+
}
48184865

48194866
count_block(dp->dp_blkstats, bp);
48204867
if (phys_birth <= scn->scn_phys.scn_min_txg ||
@@ -4891,7 +4938,9 @@ dsl_scan_scrub_done(zio_t *zio)
48914938
}
48924939

48934940
if (zio->io_error && (zio->io_error != ECKSUM ||
4894-
!(zio->io_flags & ZIO_FLAG_SPECULATIVE))) {
4941+
!(zio->io_flags & ZIO_FLAG_SPECULATIVE)) &&
4942+
!(zio->io_error == EACCES && (zio->io_flags & ZIO_FLAG_SCRUB) &&
4943+
zfs_scrub_thorough != 0)) {
48954944
if (dsl_errorscrubbing(spa->spa_dsl_pool) &&
48964945
!dsl_errorscrub_is_paused(spa->spa_dsl_pool->dp_scan)) {
48974946
atomic_inc_64(&spa->spa_dsl_pool->dp_scan
@@ -4916,7 +4965,25 @@ scan_exec_io(dsl_pool_t *dp, const blkptr_t *bp, int zio_flags,
49164965
{
49174966
spa_t *spa = dp->dp_spa;
49184967
dsl_scan_t *scn = dp->dp_scan;
4919-
size_t size = BP_GET_PSIZE(bp);
4968+
4969+
/*
4970+
* If we are trying to do a thorough scrub, the key may have
4971+
* been unloaded since we queued the IO. Check if we have the
4972+
* key and if not, fall back to a raw scrub.
4973+
*/
4974+
if ((zio_flags & ZIO_FLAG_RAW) == 0 && BP_IS_ENCRYPTED(bp)) {
4975+
dsl_crypto_key_t *dck = NULL;
4976+
int err = spa_keystore_lookup_key(spa, zb->zb_objset,
4977+
FTAG, &dck);
4978+
if (err == 0) {
4979+
spa_keystore_dsl_key_rele(spa, dck, FTAG);
4980+
} else {
4981+
zio_flags |= ZIO_FLAG_RAW;
4982+
}
4983+
}
4984+
4985+
size_t size = (zio_flags & ZIO_FLAG_RAW) ?
4986+
BP_GET_PSIZE(bp) : BP_GET_LSIZE(bp);
49204987
abd_t *data = abd_alloc_for_io(size, B_FALSE);
49214988
zio_t *pio;
49224989

@@ -5362,3 +5429,6 @@ ZFS_MODULE_PARAM(zfs, zfs_, resilver_defer_percent, UINT, ZMOD_RW,
53625429

53635430
ZFS_MODULE_PARAM(zfs, zfs_, scrub_error_blocks_per_txg, UINT, ZMOD_RW,
53645431
"Error blocks to be scrubbed in one txg");
5432+
5433+
ZFS_MODULE_PARAM(zfs, zfs_, scrub_thorough, UINT, ZMOD_RW,
5434+
"Scrub will decrypt and decompress blocks");

tests/runfiles/common.run

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ tests = ['zpool_scrub_001_neg', 'zpool_scrub_002_pos', 'zpool_scrub_003_pos',
546546
'zpool_scrub_multiple_pools',
547547
'zpool_error_scrub_001_pos', 'zpool_error_scrub_002_pos',
548548
'zpool_error_scrub_003_pos', 'zpool_error_scrub_004_pos',
549-
'zpool_scrub_date_range_001']
549+
'zpool_scrub_date_range_001', 'zpool_scrub_thorough']
550550
tags = ['functional', 'cli_root', 'zpool_scrub']
551551

552552
[tests/functional/cli_root/zpool_set]

tests/zfs-tests/include/tunables.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ SCAN_LEGACY scan_legacy zfs_scan_legacy
8181
SCAN_SUSPEND_PROGRESS scan_suspend_progress zfs_scan_suspend_progress
8282
SCAN_VDEV_LIMIT scan_vdev_limit zfs_scan_vdev_limit
8383
SCRUB_AFTER_EXPAND scrub_after_expand zfs_scrub_after_expand
84+
SCRUB_THOROUGH scrub_thorough zfs_scrub_thorough
8485
SEND_HOLES_WITHOUT_BIRTH_TIME send_holes_without_birth_time send_holes_without_birth_time
8586
SLOW_IO_EVENTS_PER_SECOND slow_io_events_per_second zfs_slow_io_events_per_second
8687
SPA_ASIZE_INFLATION spa.asize_inflation spa_asize_inflation

tests/zfs-tests/tests/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
12501250
functional/cli_root/zpool_scrub/zpool_error_scrub_002_pos.ksh \
12511251
functional/cli_root/zpool_scrub/zpool_error_scrub_003_pos.ksh \
12521252
functional/cli_root/zpool_scrub/zpool_error_scrub_004_pos.ksh \
1253+
functional/cli_root/zpool_scrub/zpool_scrub_thorough.ksh \
12531254
functional/cli_root/zpool_set/cleanup.ksh \
12541255
functional/cli_root/zpool_set/setup.ksh \
12551256
functional/cli_root/zpool/setup.ksh \
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/bin/ksh -p
2+
# SPDX-License-Identifier: CDDL-1.0
3+
#
4+
# This file and its contents are supplied under the terms of the
5+
# Common Development and Distribution License ("CDDL"), version 1.0.
6+
# You may only use this file in accordance with the terms of version
7+
# 1.0 of the CDDL.
8+
#
9+
# A full copy of the text of the CDDL should have accompanied this
10+
# source. A copy of the CDDL is also available via the Internet at
11+
# http://www.illumos.org/license/CDDL.
12+
#
13+
14+
#
15+
# Copyright (c) 2025 ConnectWise Inc. All rights reserved.
16+
#
17+
18+
. $STF_SUITE/include/libtest.shlib
19+
. $STF_SUITE/tests/functional/cli_root/zpool_scrub/zpool_scrub.cfg
20+
21+
#
22+
# DESCRIPTION:
23+
# 'zpool scrub' with thorough scrub enabled works with various configurations:
24+
# 1) Compressed dataset
25+
# 2) Compressed and encrypted dataset
26+
# 3) Encrypted dataset
27+
# 4) Compressed and encrypted dataset with keys unloaded
28+
#
29+
# STRATEGY:
30+
# 1. Create datasets for the 4 scenarios.
31+
# 2. Write data to them.
32+
# 3. Enable thorough scrub tunable.
33+
# 4. Start a scrub and wait for it to finish.
34+
# 5. Verify 0 errors.
35+
# 6. Repeat with thorough scrub disabled.
36+
#
37+
38+
function cleanup
39+
{
40+
set_tunable32 SCRUB_THOROUGH 0
41+
datasetexists $TESTPOOL/comp && destroy_dataset $TESTPOOL/comp
42+
datasetexists $TESTPOOL/comp_enc && destroy_dataset $TESTPOOL/comp_enc
43+
datasetexists $TESTPOOL/enc && destroy_dataset $TESTPOOL/enc
44+
datasetexists $TESTPOOL/comp_enc_unloaded && destroy_dataset $TESTPOOL/comp_enc_unloaded
45+
}
46+
47+
verify_runnable "global"
48+
49+
log_onexit cleanup
50+
51+
log_assert "Thorough scrub works with various dataset configurations."
52+
53+
# 1) Compressed dataset
54+
log_must zfs create $TESTPOOL/comp
55+
log_must zfs set compression=on $TESTPOOL/comp
56+
typeset file_comp="/$TESTPOOL/comp/$TESTFILE0"
57+
log_must dd if=/dev/urandom of=$file_comp bs=1024 count=1024 oflag=sync
58+
# Make sure data is compressible
59+
log_must eval "echo 'aaaaaaaa' >> $file_comp"
60+
61+
# 2) Compressed and encrypted dataset
62+
log_must eval "echo 'password' | zfs create -o encryption=on -o keyformat=passphrase $TESTPOOL/comp_enc"
63+
log_must zfs set compression=on $TESTPOOL/comp_enc
64+
typeset file_comp_enc="/$TESTPOOL/comp_enc/$TESTFILE0"
65+
log_must dd if=/dev/urandom of=$file_comp_enc bs=1024 count=1024 oflag=sync
66+
log_must eval "echo 'aaaaaaaa' >> $file_comp_enc"
67+
68+
# 3) Encrypted dataset
69+
log_must eval "echo 'password' | zfs create -o encryption=on -o keyformat=passphrase $TESTPOOL/enc"
70+
log_must zfs set compression=off $TESTPOOL/enc
71+
typeset file_enc="/$TESTPOOL/enc/$TESTFILE0"
72+
log_must dd if=/dev/urandom of=$file_enc bs=1024 count=1024 oflag=sync
73+
74+
# 4) Compressed and encrypted dataset with keys unloaded
75+
log_must eval "echo 'password' | zfs create -o encryption=on -o keyformat=passphrase $TESTPOOL/comp_enc_unloaded"
76+
log_must zfs set compression=on $TESTPOOL/comp_enc_unloaded
77+
typeset file_comp_enc_unloaded="/$TESTPOOL/comp_enc_unloaded/$TESTFILE0"
78+
log_must dd if=/dev/urandom of=$file_comp_enc_unloaded bs=1024 count=1024 oflag=sync
79+
log_must eval "echo 'aaaaaaaa' >> $file_comp_enc_unloaded"
80+
81+
log_must zfs unmount $TESTPOOL/comp_enc_unloaded
82+
log_must zfs unload-key $TESTPOOL/comp_enc_unloaded
83+
84+
# Enable thorough scrub
85+
log_must set_tunable32 SCRUB_THOROUGH 1
86+
# Run and wait for scrub
87+
log_must zpool scrub -w $TESTPOOL
88+
89+
log_must check_pool_status $TESTPOOL "scan" "with 0 errors"
90+
91+
# Disable thorough scrub
92+
log_must set_tunable32 SCRUB_THOROUGH 0
93+
# Run and wait for scrub
94+
log_must zpool scrub -w $TESTPOOL
95+
96+
log_must check_pool_status $TESTPOOL "scan" "with 0 errors"
97+
98+
log_pass "Thorough scrub works with various dataset configurations."

0 commit comments

Comments
 (0)