libzfs: don't abort receiving a raw encrypted send that carries holds

A raw send that gathers holds but not properties (zfs send -w -h,
without -p or -R) produces a compound stream whose per-filesystem
nvlist has no "props" entry.  recv_fix_encryption_hierarchy() looked
that entry up with fnvlist_lookup_nvlist(), which aborts the process
when the key is missing, so receiving such a stream of an encrypted
dataset crashed with a failed VERIFY0() after the data had already
been received.  Release builds abort the same way, since the VERIFY
family is always compiled in.

Look "props" up tolerantly and skip the keylocation fixup when it is
absent: a holds-only raw send sends no properties, and the raw
receive has already established a usable keylocation.  The
FORCE_NEW_KEY and FORCE_INHERIT fixups, which do not need
properties, still run.

A new rsend test sends an encrypted dataset with -w -h, both a full
and an incremental stream, and confirms the receive succeeds with
the holds recreated.

Reviewed-by: Brian Behlendorf <[email protected]>
Reviewed-by: Alexander Motin <[email protected]>
Signed-off-by: MorganaFuture <[email protected]>
Closes #10787
Closes #18793
This commit is contained in:
MorganaFuture authored and GitHub committed 2026-07-14 13:50:06 -07:00
1 parent 30426217ba
commit 9a02e53ad8
4 files changed
+129 -18

No files matched your search

+37 -18
View File
@@ -3462,7 +3462,15 @@ recv_fix_encryption_hierarchy(libzfs_handle_t *hdl, const char *top_zfs,
if (stream_nvfs == NULL)
continue;
stream_props = fnvlist_lookup_nvlist(stream_nvfs, "props");
/*
* A "props" nvlist is only present when the send gathered
* properties (-R, -p or -b). A raw send carrying only holds
* (zfs send -w -h) has none, so tolerate its absence rather
* than asserting.
*/
stream_props = NULL;
(void) nvlist_lookup_nvlist(stream_nvfs, "props",
&stream_props);
stream_encroot = nvlist_exists(stream_nvfs, "is_encroot");
/*
@@ -3480,30 +3488,41 @@ recv_fix_encryption_hierarchy(libzfs_handle_t *hdl, const char *top_zfs,
}
}
stream_keylocation = fnvlist_lookup_string(stream_props,
zfs_prop_to_name(ZFS_PROP_KEYLOCATION));
/*
* Refresh the properties in case the call to
* lzc_change_key() changed the value.
* Restore the keylocation only if the stream carried
* properties. A holds-only raw send has none, and the
* raw receive has already established a usable
* keylocation, so there is nothing to fix up.
*/
zfs_refresh_properties(zhp);
err = zfs_prop_get(zhp, ZFS_PROP_KEYLOCATION,
keylocation, sizeof (keylocation), NULL, NULL,
0, B_TRUE);
if (err != 0) {
zfs_close(zhp);
goto error;
}
if (stream_props != NULL) {
stream_keylocation = fnvlist_lookup_string(
stream_props,
zfs_prop_to_name(ZFS_PROP_KEYLOCATION));
if (strcmp(keylocation, stream_keylocation) != 0) {
err = zfs_prop_set(zhp,
zfs_prop_to_name(ZFS_PROP_KEYLOCATION),
stream_keylocation);
/*
* Refresh the properties in case the call to
* lzc_change_key() changed the value.
*/
zfs_refresh_properties(zhp);
err = zfs_prop_get(zhp, ZFS_PROP_KEYLOCATION,
keylocation, sizeof (keylocation), NULL,
NULL, 0, B_TRUE);
if (err != 0) {
zfs_close(zhp);
goto error;
}
if (strcmp(keylocation, stream_keylocation)
!= 0) {
err = zfs_prop_set(zhp,
zfs_prop_to_name(
ZFS_PROP_KEYLOCATION),
stream_keylocation);
if (err != 0) {
zfs_close(zhp);
goto error;
}
}
}
}
+1
View File
@@ -1009,6 +1009,7 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos',
'send-c_embedded_blocks', 'send-c_resume', 'send-cpL_varied_recsize',
'send-c_recv_dedup', 'send-L_toggle', 'send_encrypted_incremental',
'send_encrypted_freeobjects', 'send_encrypted_hierarchy',
'send_encrypted_holds',
'send_encrypted_props', 'send_encrypted_truncated_files',
'send_freeobjects', 'send_realloc_files', 'send_realloc_encrypted_files',
'send_realloc_dnode_nblkptr',
+1
View File
@@ -2156,6 +2156,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/rsend/send_encrypted_files.ksh \
functional/rsend/send_encrypted_freeobjects.ksh \
functional/rsend/send_encrypted_hierarchy.ksh \
functional/rsend/send_encrypted_holds.ksh \
functional/rsend/send_encrypted_props.ksh \
functional/rsend/send_encrypted_truncated_files.ksh \
functional/rsend/send_freeobjects.ksh \
@@ -0,0 +1,90 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# CDDL HEADER START
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source. A copy of the CDDL is also available via the Internet at
# http://www.illumos.org/license/CDDL.
#
# CDDL HEADER END
#
#
# Copyright (c) 2026 by Andrew Mochalskyi. All rights reserved.
#
. $STF_SUITE/tests/functional/rsend/rsend.kshlib
#
# DESCRIPTION:
# A raw send of an encrypted dataset that carries snapshot holds but no
# properties (zfs send -w -h, without -p/-R) must be received without
# aborting. Such a stream has no "props" nvlist, and the receive-side
# encryption-root fixup used to dereference it unconditionally and abort
# (https://github.com/openzfs/zfs/issues/10787).
#
# STRATEGY:
# 1. Create an encrypted dataset with some data and a held snapshot.
# 2. Raw send it with holds, receive it, and confirm the receive
# succeeds and the hold is recreated.
# 3. Repeat for an incremental raw send with holds.
#
verify_runnable "both"
function cleanup
{
eval "zfs holds -H $snap 2>/dev/null | grep -q hold1" &&
log_must zfs release hold1 $snap
eval "zfs holds -H $snap2 2>/dev/null | grep -q hold2" &&
log_must zfs release hold2 $snap2
eval "zfs holds -H $recvsnap 2>/dev/null | grep -q hold1" &&
log_must zfs release hold1 $recvsnap
eval "zfs holds -H $recvsnap2 2>/dev/null | grep -q hold2" &&
log_must zfs release hold2 $recvsnap2
datasetexists $TESTPOOL/recv && destroy_dataset $TESTPOOL/recv "-r"
datasetexists $TESTPOOL/crypt && destroy_dataset $TESTPOOL/crypt "-r"
[[ -f $keyfile ]] && log_must rm -f $keyfile
[[ -f $sendfile ]] && log_must rm -f $sendfile
}
log_onexit cleanup
log_assert "Raw send of an encrypted dataset with holds is received cleanly"
typeset keyfile=/$TESTPOOL/pkey.holds
typeset sendfile=$TEST_BASE_DIR/send_encrypted_holds.$$
typeset snap=$TESTPOOL/crypt@snap1
typeset snap2=$TESTPOOL/crypt@snap2
typeset recvsnap=$TESTPOOL/recv@snap1
typeset recvsnap2=$TESTPOOL/recv@snap2
log_must eval "echo 'password' > $keyfile"
log_must zfs create -o encryption=on -o keyformat=passphrase \
-o keylocation=file://$keyfile $TESTPOOL/crypt
log_must mkfile 1m /$TESTPOOL/crypt/file1
log_must zfs snapshot $snap
log_must zfs hold hold1 $snap
log_note "Full raw send with holds is received without aborting"
log_must eval "zfs send -w -h $snap > $sendfile"
log_must eval "zfs receive $TESTPOOL/recv < $sendfile"
log_must datasetexists $recvsnap
log_must eval "zfs holds -H $recvsnap | grep -q hold1"
log_note "Incremental raw send with holds is received without aborting"
log_must mkfile 1m /$TESTPOOL/crypt/file2
log_must zfs snapshot $snap2
log_must zfs hold hold2 $snap2
log_must eval "zfs send -w -h -i $snap $snap2 > $sendfile"
log_must eval "zfs receive $TESTPOOL/recv < $sendfile"
log_must datasetexists $recvsnap2
log_must eval "zfs holds -H $recvsnap2 | grep -q hold2"
log_pass "Raw send of an encrypted dataset with holds is received cleanly"