Skip to content

Commit ff9eb7a

Browse files
oromenaharmmatuska
authored andcommitted
Allow block cloning across encrypted datasets
When two datasets share the same master encryption key, it is safe to clone encrypted blocks. Currently only snapshots and clones of a dataset share with it the same encryption key. Added a test for: - Clone from encrypted sibling to encrypted sibling with non encrypted parent - Clone from encrypted parent to inherited encrypted child - Clone from child to sibling with encrypted parent - Clone from snapshot to the original datasets - Clone from foreign snapshot to a foreign dataset - Cloning from non-encrypted to encrypted datasets - Cloning from encrypted to non-encrypted datasets Reviewed-by: Alexander Motin <mav@FreeBSD.org> Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov> Original-patch-by: Pawel Jakub Dawidek <pawel@dawidek.net> Signed-off-by: Kay Pedersen <mail@mkwg.de> Closes openzfs#15544
1 parent d2ff592 commit ff9eb7a

File tree

10 files changed

+236
-25
lines changed

10 files changed

+236
-25
lines changed

include/sys/dsl_crypt.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ void dsl_dataset_promote_crypt_sync(dsl_dir_t *target, dsl_dir_t *origin,
206206
dmu_tx_t *tx);
207207
int dmu_objset_create_crypt_check(dsl_dir_t *parentdd,
208208
dsl_crypto_params_t *dcp, boolean_t *will_encrypt);
209+
boolean_t dmu_objset_crypto_key_equal(objset_t *osa, objset_t *osb);
209210
void dsl_dataset_create_crypt_sync(uint64_t dsobj, dsl_dir_t *dd,
210211
struct dsl_dataset *origin, dsl_crypto_params_t *dcp, dmu_tx_t *tx);
211212
uint64_t dsl_crypto_key_create_sync(uint64_t crypt, dsl_wrapping_key_t *wkey,

man/man7/zpool-features.7

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,12 @@ When this feature is enabled ZFS will use block cloning for operations like
364364
Block cloning allows to create multiple references to a single block.
365365
It is much faster than copying the data (as the actual data is neither read nor
366366
written) and takes no additional space.
367-
Blocks can be cloned across datasets under some conditions (like disabled
368-
encryption and equal
369-
.Nm recordsize ) .
367+
Blocks can be cloned across datasets under some conditions (like equal
368+
.Nm recordsize ,
369+
the same master encryption key, etc.).
370+
ZFS tries its best to clone across datasets including encrypted ones.
371+
This is limited for various (nontrivial) reasons depending on the OS
372+
and/or ZFS internals.
370373
.Pp
371374
This feature becomes
372375
.Sy active

module/zfs/brt.c

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,8 @@
157157
* (copying the file content to the new dataset and removing the source file).
158158
* In that case Block Cloning will only be used briefly, because the BRT entries
159159
* will be removed when the source is removed.
160-
* Note: currently it is not possible to clone blocks between encrypted
161-
* datasets, even if those datasets use the same encryption key (this includes
162-
* snapshots of encrypted datasets). Cloning blocks between datasets that use
163-
* the same keys should be possible and should be implemented in the future.
160+
* Block Cloning across encrypted datasets is supported as long as both
161+
* datasets share the same master key (e.g. snapshots and clones)
164162
*
165163
* Block Cloning flow through ZFS layers.
166164
*

module/zfs/dsl_crypt.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,40 @@ spa_crypto_key_compare(const void *a, const void *b)
266266
return (0);
267267
}
268268

269+
/*
270+
* this compares a crypto key based on zk_guid. See comment on
271+
* spa_crypto_key_compare for more information.
272+
*/
273+
boolean_t
274+
dmu_objset_crypto_key_equal(objset_t *osa, objset_t *osb)
275+
{
276+
dsl_crypto_key_t *dcka = NULL;
277+
dsl_crypto_key_t *dckb = NULL;
278+
uint64_t obja, objb;
279+
boolean_t equal;
280+
spa_t *spa;
281+
282+
spa = dmu_objset_spa(osa);
283+
if (spa != dmu_objset_spa(osb))
284+
return (B_FALSE);
285+
obja = dmu_objset_ds(osa)->ds_object;
286+
objb = dmu_objset_ds(osb)->ds_object;
287+
288+
if (spa_keystore_lookup_key(spa, obja, FTAG, &dcka) != 0)
289+
return (B_FALSE);
290+
if (spa_keystore_lookup_key(spa, objb, FTAG, &dckb) != 0) {
291+
spa_keystore_dsl_key_rele(spa, dcka, FTAG);
292+
return (B_FALSE);
293+
}
294+
295+
equal = (dcka->dck_key.zk_guid == dckb->dck_key.zk_guid);
296+
297+
spa_keystore_dsl_key_rele(spa, dcka, FTAG);
298+
spa_keystore_dsl_key_rele(spa, dckb, FTAG);
299+
300+
return (equal);
301+
}
302+
269303
static int
270304
spa_key_mapping_compare(const void *a, const void *b)
271305
{

module/zfs/zfs_vnops.c

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
#include <sys/fs/zfs.h>
4848
#include <sys/dmu.h>
4949
#include <sys/dmu_objset.h>
50+
#include <sys/dsl_crypt.h>
5051
#include <sys/spa.h>
5152
#include <sys/txg.h>
5253
#include <sys/dbuf.h>
@@ -1103,6 +1104,16 @@ zfs_clone_range(znode_t *inzp, uint64_t *inoffp, znode_t *outzp,
11031104
return (SET_ERROR(EXDEV));
11041105
}
11051106

1107+
/*
1108+
* Cloning across encrypted datasets is possible only if they
1109+
* share the same master key.
1110+
*/
1111+
if (inos != outos && inos->os_encrypted &&
1112+
!dmu_objset_crypto_key_equal(inos, outos)) {
1113+
zfs_exit_two(inzfsvfs, outzfsvfs, FTAG);
1114+
return (SET_ERROR(EXDEV));
1115+
}
1116+
11061117
error = zfs_verify_zp(inzp);
11071118
if (error == 0)
11081119
error = zfs_verify_zp(outzp);
@@ -1286,20 +1297,6 @@ zfs_clone_range(znode_t *inzp, uint64_t *inoffp, znode_t *outzp,
12861297
*/
12871298
break;
12881299
}
1289-
/*
1290-
* Encrypted data is fine as long as it comes from the same
1291-
* dataset.
1292-
* TODO: We want to extend it in the future to allow cloning to
1293-
* datasets with the same keys, like clones or to be able to
1294-
* clone a file from a snapshot of an encrypted dataset into the
1295-
* dataset itself.
1296-
*/
1297-
if (BP_IS_PROTECTED(&bps[0])) {
1298-
if (inzfsvfs != outzfsvfs) {
1299-
error = SET_ERROR(EXDEV);
1300-
break;
1301-
}
1302-
}
13031300

13041301
/*
13051302
* Start a transaction.

tests/runfiles/linux.run

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ tests = ['block_cloning_copyfilerange', 'block_cloning_copyfilerange_partial',
4242
'block_cloning_disabled_copyfilerange', 'block_cloning_disabled_ficlone',
4343
'block_cloning_disabled_ficlonerange',
4444
'block_cloning_copyfilerange_cross_dataset',
45+
'block_cloning_cross_enc_dataset',
4546
'block_cloning_copyfilerange_fallback_same_txg']
4647
tags = ['functional', 'block_cloning']
4748

tests/test-runner/bin/zts-report.py.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ elif sys.platform.startswith('linux'):
305305
['SKIP', cfr_cross_reason],
306306
'block_cloning/block_cloning_copyfilerange_fallback_same_txg':
307307
['SKIP', cfr_cross_reason],
308+
'block_cloning/block_cloning_cross_enc_dataset':
309+
['SKIP', cfr_cross_reason],
308310
})
309311

310312

tests/zfs-tests/tests/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
451451
functional/block_cloning/block_cloning_ficlone.ksh \
452452
functional/block_cloning/block_cloning_ficlonerange.ksh \
453453
functional/block_cloning/block_cloning_ficlonerange_partial.ksh \
454+
functional/block_cloning/block_cloning_cross_enc_dataset.ksh \
454455
functional/bootfs/bootfs_001_pos.ksh \
455456
functional/bootfs/bootfs_002_neg.ksh \
456457
functional/bootfs/bootfs_003_pos.ksh \

tests/zfs-tests/tests/functional/block_cloning/block_cloning.kshlib

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828

2929
function have_same_content
3030
{
31-
typeset hash1=$(cat $1 | md5sum)
32-
typeset hash2=$(cat $2 | md5sum)
31+
typeset hash1=$(md5digest $1)
32+
typeset hash2=$(md5digest $2)
3333

3434
log_must [ "$hash1" = "$hash2" ]
3535
}
@@ -44,10 +44,14 @@ function have_same_content
4444
#
4545
function get_same_blocks
4646
{
47+
KEY=$5
48+
if [ ${#KEY} -gt 0 ]; then
49+
KEY="--key=$KEY"
50+
fi
4751
typeset zdbout=${TMPDIR:-$TEST_BASE_DIR}/zdbout.$$
48-
zdb -vvvvv $1 -O $2 | \
52+
zdb $KEY -vvvvv $1 -O $2 | \
4953
awk '/ L0 / { print l++ " " $3 " " $7 }' > $zdbout.a
50-
zdb -vvvvv $3 -O $4 | \
54+
zdb $KEY -vvvvv $3 -O $4 | \
5155
awk '/ L0 / { print l++ " " $3 " " $7 }' > $zdbout.b
5256
echo $(sort $zdbout.a $zdbout.b | uniq -d | cut -f1 -d' ')
5357
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/bin/ksh -p
2+
#
3+
# CDDL HEADER START
4+
#
5+
# The contents of this file are subject to the terms of the
6+
# Common Development and Distribution License (the "License").
7+
# You may not use this file except in compliance with the License.
8+
#
9+
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10+
# or https://opensource.org/licenses/CDDL-1.0.
11+
# See the License for the specific language governing permissions
12+
# and limitations under the License.
13+
#
14+
# When distributing Covered Code, include this CDDL HEADER in each
15+
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16+
# If applicable, add the following below this CDDL HEADER, with the
17+
# fields enclosed by brackets "[]" replaced with your own identifying
18+
# information: Portions Copyright [yyyy] [name of copyright owner]
19+
#
20+
# CDDL HEADER END
21+
#
22+
23+
#
24+
# Copyright (c) 2023, Kay Pedersen <mail@mkwg.de>
25+
#
26+
27+
. $STF_SUITE/include/libtest.shlib
28+
. $STF_SUITE/tests/functional/block_cloning/block_cloning.kshlib
29+
30+
verify_runnable "global"
31+
32+
if [[ $(linux_version) -lt $(linux_version "5.3") ]]; then
33+
log_unsupported "copy_file_range can't copy cross-filesystem before Linux 5.3"
34+
fi
35+
36+
claim="Block cloning across encrypted datasets."
37+
38+
log_assert $claim
39+
40+
DS1="$TESTPOOL/encrypted1"
41+
DS2="$TESTPOOL/encrypted2"
42+
DS1_NC="$TESTPOOL/notcrypted1"
43+
PASSPHRASE="top_secret"
44+
45+
function prepare_enc
46+
{
47+
log_must zpool create -o feature@block_cloning=enabled $TESTPOOL $DISKS
48+
log_must eval "echo $PASSPHRASE | zfs create -o encryption=on" \
49+
"-o keyformat=passphrase -o keylocation=prompt $DS1"
50+
log_must eval "echo $PASSPHRASE | zfs create -o encryption=on" \
51+
"-o keyformat=passphrase -o keylocation=prompt $DS2"
52+
log_must zfs create $DS1/child1
53+
log_must zfs create $DS1/child2
54+
log_must zfs create $DS1_NC
55+
56+
log_note "Create test file"
57+
# we must wait until the src file txg is written to the disk otherwise we
58+
# will fallback to normal copy. See "dmu_read_l0_bps" in
59+
# "zfs/module/zfs/dmu.c" and "zfs_clone_range" in
60+
# "zfs/module/zfs/zfs_vnops.c"
61+
log_must dd if=/dev/urandom of=/$DS1/file bs=128K count=4
62+
log_must dd if=/dev/urandom of=/$DS1/child1/file bs=128K count=4
63+
log_must dd if=/dev/urandom of=/$DS1_NC/file bs=128K count=4
64+
log_must sync_pool $TESTPOOL
65+
}
66+
67+
function cleanup_enc
68+
{
69+
datasetexists $TESTPOOL && destroy_pool $TESTPOOL
70+
}
71+
72+
function clone_and_check
73+
{
74+
I_FILE="$1"
75+
O_FILE=$2
76+
I_DS=$3
77+
O_DS=$4
78+
SAME_BLOCKS=$5
79+
# the CLONE option provides a choice between copy_file_range
80+
# which should clone and a dd which is a copy no matter what
81+
CLONE=$6
82+
SNAPSHOT=$7
83+
if [ ${#SNAPSHOT} -gt 0 ]; then
84+
I_FILE=".zfs/snapshot/$SNAPSHOT/$1"
85+
fi
86+
if [ $CLONE ]; then
87+
log_must clonefile -f "/$I_DS/$I_FILE" "/$O_DS/$O_FILE" 0 0 524288
88+
else
89+
log_must dd if="/$I_DS/$I_FILE" of="/$O_DS/$O_FILE" bs=128K
90+
fi
91+
log_must sync_pool $TESTPOOL
92+
93+
log_must have_same_content "/$I_DS/$I_FILE" "/$O_DS/$O_FILE"
94+
95+
if [ ${#SNAPSHOT} -gt 0 ]; then
96+
I_DS="$I_DS@$SNAPSHOT"
97+
I_FILE="$1"
98+
fi
99+
typeset blocks=$(get_same_blocks \
100+
$I_DS $I_FILE $O_DS $O_FILE $PASSPHRASE)
101+
log_must [ "$blocks" = "$SAME_BLOCKS" ]
102+
}
103+
104+
log_onexit cleanup_enc
105+
106+
prepare_enc
107+
108+
log_note "Cloning entire file with copy_file_range across different enc" \
109+
"roots, should fallback"
110+
# we are expecting no same block map.
111+
clone_and_check "file" "clone" $DS1 $DS2 "" true
112+
log_note "check if the file is still readable and the same after" \
113+
"unmount and key unload, shouldn't fail"
114+
typeset hash1=$(md5digest "/$DS1/file")
115+
log_must zfs umount $DS1 && zfs unload-key $DS1
116+
typeset hash2=$(md5digest "/$DS2/clone")
117+
log_must [ "$hash1" = "$hash2" ]
118+
119+
cleanup_enc
120+
prepare_enc
121+
122+
log_note "Cloning entire file with copy_file_range across different child datasets"
123+
# clone shouldn't work because of deriving a new master key for the child
124+
# we are expecting no same block map.
125+
clone_and_check "file" "clone" $DS1 "$DS1/child1" "" true
126+
clone_and_check "file" "clone" "$DS1/child1" "$DS1/child2" "" true
127+
128+
cleanup_enc
129+
prepare_enc
130+
131+
log_note "Copying entire file with copy_file_range across same snapshot"
132+
log_must zfs snapshot -r $DS1@s1
133+
log_must sync_pool $TESTPOOL
134+
log_must rm -f "/$DS1/file"
135+
log_must sync_pool $TESTPOOL
136+
clone_and_check "file" "clone" "$DS1" "$DS1" "0 1 2 3" true "s1"
137+
138+
cleanup_enc
139+
prepare_enc
140+
141+
log_note "Copying entire file with copy_file_range across different snapshot"
142+
clone_and_check "file" "file" $DS1 $DS2 "" true
143+
log_must zfs snapshot -r $DS2@s1
144+
log_must sync_pool $TESTPOOL
145+
log_must rm -f "/$DS1/file" "/$DS2/file"
146+
log_must sync_pool $TESTPOOL
147+
clone_and_check "file" "clone" "$DS2" "$DS1" "" true "s1"
148+
typeset hash1=$(md5digest "/$DS1/.zfs/snapshot/s1/file")
149+
log_note "destroy the snapshot and check if the file is still readable and" \
150+
"has the same content"
151+
log_must zfs destroy -r $DS2@s1
152+
log_must sync_pool $TESTPOOL
153+
typeset hash2=$(md5digest "/$DS1/file")
154+
log_must [ "$hash1" = "$hash2" ]
155+
156+
cleanup_enc
157+
prepare_enc
158+
159+
log_note "Copying with copy_file_range from non encrypted to encrypted"
160+
clone_and_check "file" "copy" $DS1_NC $DS1 "" true
161+
162+
cleanup_enc
163+
prepare_enc
164+
165+
log_note "Copying with copy_file_range from encrypted to non encrypted"
166+
clone_and_check "file" "copy" $DS1 $DS1_NC "" true
167+
168+
log_must sync_pool $TESTPOOL
169+
170+
log_pass $claim

0 commit comments

Comments
 (0)