From d16a8b220dfc0934c72c0bf8f4c13a51c170b273 Mon Sep 17 00:00:00 2001 From: Tom Caputi Date: Fri, 18 Jan 2019 02:21:01 -0500 Subject: [PATCH] Detect and prevend mixed raw and non-raw sends Currently, there is an issue in the raw receive code where raw receives are allowed to happen on top of previously non-raw received datasets. This is a problem because the source-side dataset doesn't know about how the blocks on the destination were encrypted. As a result, any MAC in the objset's checksum-of-MACs tree that is a parent of both blocks encrypted on the source and blocks encrypted by the destination will be incorrect. This will result in authentication errors when we decrypt the dataset. This patch fixes this issue by adding a new check to the raw receive code. The code now maintains an "IVset guid", which acts as an identifier for the set of IVs used to encrypt a given snapshot. When a snapshot is raw received, the destination snapshot will take this value from the DRR_BEGIN payload. Non-raw receives and normal "zfs snap" operations will cause ZFS to generate a new IVset guid. When a raw incremental stream is received, ZFS will check that the "from" IVset guid in the stream matches that of the "from" destination snapshot. If they do not match, the code will error out the receive, preventing the problem. Signed-off-by: Tom Caputi --- contrib/pyzfs/setup.py | 61 +++++++++ include/sys/dmu_recv.h | 2 + include/sys/dsl_bookmark.h | 17 +++ include/sys/dsl_crypt.h | 5 +- include/sys/dsl_dataset.h | 6 + include/sys/fs/zfs.h | 2 + lib/libzfs/libzfs_sendrecv.c | 6 + man/man8/zfs.8 | 25 ++++ module/zcommon/zfs_prop.c | 2 + module/zfs/dmu_recv.c | 42 +++++-- module/zfs/dmu_send.c | 34 ++++- module/zfs/dsl_bookmark.c | 25 +++- module/zfs/dsl_crypt.c | 84 ++++++++++--- module/zfs/dsl_dataset.c | 12 ++ module/zfs/zcp_get.c | 5 + tests/runfiles/linux.run | 2 +- .../tests/functional/rsend/Makefile.am | 1 + .../tests/functional/rsend/send_mixed_raw.ksh | 118 ++++++++++++++++++ 18 files changed, 410 insertions(+), 39 deletions(-) create mode 100644 contrib/pyzfs/setup.py create mode 100755 tests/zfs-tests/tests/functional/rsend/send_mixed_raw.ksh diff --git a/contrib/pyzfs/setup.py b/contrib/pyzfs/setup.py new file mode 100644 index 000000000000..22e609951c7f --- /dev/null +++ b/contrib/pyzfs/setup.py @@ -0,0 +1,61 @@ +# +# Copyright 2015 ClusterHQ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import absolute_import, division, print_function + +from setuptools import setup, find_packages + +setup( + name="pyzfs", + version="0.8.0", + description="Wrapper for libzfs_core", + author="ClusterHQ", + author_email="support@clusterhq.com", + url="http://pyzfs.readthedocs.org", + license="Apache License, Version 2.0", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: System :: Filesystems", + "Topic :: Software Development :: Libraries", + ], + keywords=[ + "ZFS", + "OpenZFS", + "libzfs_core", + ], + + packages=find_packages(), + include_package_data=True, + install_requires=[ + "cffi", + ], + setup_requires=[ + "cffi", + ], + python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,<4', + zip_safe=False, + test_suite="libzfs_core.test", +) + +# vim: softtabstop=4 tabstop=4 expandtab shiftwidth=4 diff --git a/include/sys/dmu_recv.h b/include/sys/dmu_recv.h index 4a0d47711589..90002026bec9 100644 --- a/include/sys/dmu_recv.h +++ b/include/sys/dmu_recv.h @@ -51,7 +51,9 @@ typedef struct dmu_recv_cookie { struct avl_tree *drc_guid_to_ds_map; nvlist_t *drc_keynvl; zio_cksum_t drc_cksum; + uint64_t drc_fromsnapobj; uint64_t drc_newsnapobj; + uint64_t drc_ivset_guid; void *drc_owner; cred_t *drc_cred; } dmu_recv_cookie_t; diff --git a/include/sys/dsl_bookmark.h b/include/sys/dsl_bookmark.h index e477bb231c6c..3cdad7441407 100644 --- a/include/sys/dsl_bookmark.h +++ b/include/sys/dsl_bookmark.h @@ -36,8 +36,25 @@ typedef struct zfs_bookmark_phys { uint64_t zbm_guid; /* guid of bookmarked dataset */ uint64_t zbm_creation_txg; /* birth transaction group */ uint64_t zbm_creation_time; /* bookmark creation time */ + + /* the following fields are reserved for redacted send / recv */ + uint64_t zbm_redaction_obj; /* redaction list object */ + uint64_t zbm_flags; /* ZBM_FLAG_* */ + uint64_t zbm_referenced_bytes_refd; + uint64_t zbm_compressed_bytes_refd; + uint64_t zbm_uncompressed_bytes_refd; + uint64_t zbm_referenced_freed_before_next_snap; + uint64_t zbm_compressed_freed_before_next_snap; + uint64_t zbm_uncompressed_freed_before_next_snap; + + /* fields used for raw sends */ + uint64_t zbm_ivset_guid; } zfs_bookmark_phys_t; + +#define BOOKMARK_PHYS_SIZE_V1 (3 * sizeof (uint64_t)) +#define BOOKMARK_PHYS_SIZE_V2 (12 * sizeof (uint64_t)) + int dsl_bookmark_create(nvlist_t *, nvlist_t *); int dsl_get_bookmarks(const char *, nvlist_t *, nvlist_t *); int dsl_get_bookmarks_impl(dsl_dataset_t *, nvlist_t *, nvlist_t *); diff --git a/include/sys/dsl_crypt.h b/include/sys/dsl_crypt.h index e01d53527e7e..903769bb5fe1 100644 --- a/include/sys/dsl_crypt.h +++ b/include/sys/dsl_crypt.h @@ -189,12 +189,13 @@ void key_mapping_rele(spa_t *spa, dsl_key_mapping_t *km, void *tag); int spa_keystore_lookup_key(spa_t *spa, uint64_t dsobj, void *tag, dsl_crypto_key_t **dck_out); -int dsl_crypto_populate_key_nvlist(struct dsl_dataset *ds, nvlist_t **nvl_out); +int dsl_crypto_populate_key_nvlist(struct dsl_dataset *ds, + uint64_t from_ivset_guid, nvlist_t **nvl_out); int dsl_crypto_recv_raw_key_check(struct dsl_dataset *ds, nvlist_t *nvl, dmu_tx_t *tx); void dsl_crypto_recv_raw_key_sync(struct dsl_dataset *ds, nvlist_t *nvl, dmu_tx_t *tx); -int dsl_crypto_recv_raw(const char *poolname, uint64_t dsobj, +int dsl_crypto_recv_raw(const char *poolname, uint64_t dsobj, uint64_t frombj, dmu_objset_type_t ostype, nvlist_t *nvl, boolean_t do_key); int spa_keystore_change_key(const char *dsname, dsl_crypto_params_t *dcp); diff --git a/include/sys/dsl_dataset.h b/include/sys/dsl_dataset.h index 47a46f07d3fe..c464c70bd2da 100644 --- a/include/sys/dsl_dataset.h +++ b/include/sys/dsl_dataset.h @@ -114,6 +114,12 @@ struct dsl_key_mapping; */ #define DS_FIELD_REMAP_DEADLIST "com.delphix:remap_deadlist" +/* + * This field is set to the ivset guid for encrypted snapshots. This is used + * for validating raw receives. + */ +#define DS_FIELD_IVSET_GUID "com.datto:ivset_guid" + /* * DS_FLAG_CI_DATASET is set if the dataset contains a file system whose * name lookups should be performed case-insensitively. diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h index 945853739b7c..96eab234f8cd 100644 --- a/include/sys/fs/zfs.h +++ b/include/sys/fs/zfs.h @@ -183,6 +183,7 @@ typedef enum { ZFS_PROP_KEYSTATUS, ZFS_PROP_REMAPTXG, /* not exposed to the user */ ZFS_PROP_SPECIAL_SMALL_BLOCKS, + ZFS_PROP_IVSET_GUID, ZFS_NUM_PROPS } zfs_prop_t; @@ -1256,6 +1257,7 @@ typedef enum { ZFS_ERR_IOC_ARG_UNAVAIL, ZFS_ERR_IOC_ARG_REQUIRED, ZFS_ERR_IOC_ARG_BADTYPE, + ZFS_ERR_FROM_IVSET_GUID_MISMATCH, } zfs_errno_t; /* diff --git a/lib/libzfs/libzfs_sendrecv.c b/lib/libzfs/libzfs_sendrecv.c index ae24454d7c79..3a9482740236 100644 --- a/lib/libzfs/libzfs_sendrecv.c +++ b/lib/libzfs/libzfs_sendrecv.c @@ -4281,6 +4281,12 @@ zfs_receive_one(libzfs_handle_t *hdl, int infd, const char *tosnap, "destination %s space quota exceeded."), name); (void) zfs_error(hdl, EZFS_NOSPC, errbuf); break; + case ZFS_ERR_FROM_IVSET_GUID_MISMATCH: + zfs_error_aux(hdl, dgettext(TEXT_DOMAIN, + "IV set guid mismatch. See man page for " + "best practices with raw sends.")); + (void) zfs_error(hdl, EZFS_BADSTREAM, errbuf); + break; case EBUSY: if (hastoken) { zfs_error_aux(hdl, dgettext(TEXT_DOMAIN, diff --git a/man/man8/zfs.8 b/man/man8/zfs.8 index df904d0bdb20..4bb7127ffb36 100644 --- a/man/man8/zfs.8 +++ b/man/man8/zfs.8 @@ -3829,6 +3829,31 @@ parameters with the .Fl o options. .Pp +The added security provided by raw sends adds some restrictions to the send +and receive process. ZFS will not allow a mix of raw sends and non-raw sends. +Specifically, any raw incremental sends that are attempted after a non-raw +send is received will fail. Non-raw sends do not have this restriction and, +therefore, are always possible. Because of this, it is best practice to always +use either raw sends for their security benefits or non-raw sends for their +flexibility when working with encrypted datasets, but not a combination. +.Pp +The reason for this restriction stems from the inherent restrictions of the +AEAD ciphers that ZFS uses to encrypt data. When using ZFS native encryption, +each block of data is encrypted against a randomly generated number known as +the "initialization vector" (IV), which is stored in the filesystem metadata. +This number is required by the encryption algorithms whenever the data is to +be decrypted. Together, all of the IVs provided for all of the blocks a given +snapshot are collectively called an "IV set". When ZFS performs a raw send, +the IV set is transferred from the source to the destination in the send +stream. When ZFS performs a non-raw send, the data is decrypted by the source +system and re-encrypted by the destination system, creating a snapshot with +effectively the same data, but a different IV set. In order for decryption to +work after a raw send, ZFS must ensure that the IV set used on both the source +and destination side match. When an incremental raw receive is performed on +top of an existing snapshot, ZFS will check to confirm that the "from" +snapshot on both the source and destination were using the same IV set, +ensuring the new IV set is consistent. +.Pp The name of the snapshot .Pq and file system, if a full stream is received that this subcommand creates depends on the argument type and the use of the diff --git a/module/zcommon/zfs_prop.c b/module/zcommon/zfs_prop.c index 4d5bc39e5a4a..e25023d6a9fe 100644 --- a/module/zcommon/zfs_prop.c +++ b/module/zcommon/zfs_prop.c @@ -559,6 +559,8 @@ zfs_prop_init(void) PROP_READONLY, ZFS_TYPE_DATASET, "UNIQUE"); zprop_register_hidden(ZFS_PROP_INCONSISTENT, "inconsistent", PROP_TYPE_NUMBER, PROP_READONLY, ZFS_TYPE_DATASET, "INCONSISTENT"); + zprop_register_hidden(ZFS_PROP_IVSET_GUID, "ivsetguid", + PROP_TYPE_NUMBER, PROP_READONLY, ZFS_TYPE_SNAPSHOT, "IVSETGUID"); zprop_register_hidden(ZFS_PROP_PREV_SNAP, "prevsnap", PROP_TYPE_STRING, PROP_READONLY, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME, "PREVSNAP"); zprop_register_hidden(ZFS_PROP_PBKDF2_SALT, "pbkdf2salt", diff --git a/module/zfs/dmu_recv.c b/module/zfs/dmu_recv.c index 990f790256be..3a20ccef87b5 100644 --- a/module/zfs/dmu_recv.c +++ b/module/zfs/dmu_recv.c @@ -71,7 +71,6 @@ typedef struct dmu_recv_begin_arg { dmu_recv_cookie_t *drba_cookie; cred_t *drba_cred; dsl_crypto_params_t *drba_dcp; - uint64_t drba_snapobj; } dmu_recv_begin_arg_t; static int @@ -117,7 +116,7 @@ recv_begin_check_existing_impl(dmu_recv_begin_arg_t *drba, dsl_dataset_t *ds, dsl_dataset_t *snap; uint64_t obj = dsl_dataset_phys(ds)->ds_prev_snap_obj; - /* Can't perform a raw receive on top of a non-raw receive */ + /* Can't raw receive on top of an unencrypted dataset */ if (!encrypted && raw) return (SET_ERROR(EINVAL)); @@ -144,7 +143,7 @@ recv_begin_check_existing_impl(dmu_recv_begin_arg_t *drba, dsl_dataset_t *ds, return (SET_ERROR(ENODEV)); if (drba->drba_cookie->drc_force) { - drba->drba_snapobj = obj; + drba->drba_cookie->drc_fromsnapobj = obj; } else { /* * If we are not forcing, there must be no @@ -154,7 +153,8 @@ recv_begin_check_existing_impl(dmu_recv_begin_arg_t *drba, dsl_dataset_t *ds, dsl_dataset_rele(snap, FTAG); return (SET_ERROR(ETXTBSY)); } - drba->drba_snapobj = ds->ds_prev->ds_object; + drba->drba_cookie->drc_fromsnapobj = + ds->ds_prev->ds_object; } dsl_dataset_rele(snap, FTAG); @@ -189,7 +189,7 @@ recv_begin_check_existing_impl(dmu_recv_begin_arg_t *drba, dsl_dataset_t *ds, return (SET_ERROR(EINVAL)); } - drba->drba_snapobj = 0; + drba->drba_cookie->drc_fromsnapobj = 0; } return (0); @@ -416,7 +416,7 @@ dmu_recv_begin_sync(void *arg, dmu_tx_t *tx) * the raw cmd set. Raw incremental recvs do not use a dcp * since the encryption parameters are already set in stone. */ - if (dcp == NULL && drba->drba_snapobj == 0 && + if (dcp == NULL && drba->drba_cookie->drc_fromsnapobj == 0 && drba->drba_origin == NULL) { ASSERT3P(dcp, ==, NULL); dcp = &dummy_dcp; @@ -430,15 +430,15 @@ dmu_recv_begin_sync(void *arg, dmu_tx_t *tx) /* create temporary clone */ dsl_dataset_t *snap = NULL; - if (drba->drba_snapobj != 0) { + if (drba->drba_cookie->drc_fromsnapobj != 0) { VERIFY0(dsl_dataset_hold_obj(dp, - drba->drba_snapobj, FTAG, &snap)); + drba->drba_cookie->drc_fromsnapobj, FTAG, &snap)); ASSERT3P(dcp, ==, NULL); } dsobj = dsl_dataset_create_sync(ds->ds_dir, recv_clone_name, snap, crflags, drba->drba_cred, dcp, tx); - if (drba->drba_snapobj != 0) + if (drba->drba_cookie->drc_fromsnapobj != 0) dsl_dataset_rele(snap, FTAG); dsl_dataset_rele_flags(ds, dsflags, FTAG); } else { @@ -2449,11 +2449,15 @@ dmu_recv_stream(dmu_recv_cookie_t *drc, vnode_t *vp, offset_t *voffp, * the keynvl away until then. */ err = dsl_crypto_recv_raw(spa_name(ra->os->os_spa), - drc->drc_ds->ds_object, drc->drc_drrb->drr_type, - keynvl, drc->drc_newfs); + drc->drc_ds->ds_object, drc->drc_fromsnapobj, + drc->drc_drrb->drr_type, keynvl, drc->drc_newfs); if (err != 0) goto out; + /* see comment in dmu_recv_end_sync() */ + (void) nvlist_lookup_uint64(keynvl, "to_ivset_guid", + &drc->drc_ivset_guid); + if (!drc->drc_newfs) drc->drc_keynvl = fnvlist_dup(keynvl); } @@ -2761,6 +2765,22 @@ dmu_recv_end_sync(void *arg, dmu_tx_t *tx) drc->drc_newsnapobj = dsl_dataset_phys(drc->drc_ds)->ds_prev_snap_obj; } + + /* + * If this is a raw receive, the crypt_keydata nvlist will include + * a to_vset_guid for us to set on the new snapshot. Older + * implementations of the raw send code did not have this field. + * In this case the ivset guid will be set to 0 which will disable + * the check for this value. + */ + if (drc->drc_raw) { + dmu_object_zapify(dp->dp_meta_objset, drc->drc_newsnapobj, + DMU_OT_DSL_DATASET, tx); + VERIFY0(zap_update(dp->dp_meta_objset, drc->drc_newsnapobj, + DS_FIELD_IVSET_GUID, sizeof (uint64_t), 1, + &drc->drc_ivset_guid, tx)); + } + zvol_create_minors(dp->dp_spa, drc->drc_tofs, B_TRUE); /* diff --git a/module/zfs/dmu_send.c b/module/zfs/dmu_send.c index 9c0ad406b136..dd943fd1e207 100644 --- a/module/zfs/dmu_send.c +++ b/module/zfs/dmu_send.c @@ -1119,9 +1119,13 @@ dmu_send_impl(void *tag, dsl_pool_t *dp, dsl_dataset_t *to_ds, } if (featureflags & DMU_BACKUP_FEATURE_RAW) { + uint64_t ivset_guid = (ancestor_zb != NULL) ? + ancestor_zb->zbm_ivset_guid : 0; + ASSERT(os->os_encrypted); - err = dsl_crypto_populate_key_nvlist(to_ds, &keynvl); + err = dsl_crypto_populate_key_nvlist(to_ds, + ivset_guid, &keynvl); if (err != 0) { fnvlist_free(nvl); goto out; @@ -1235,7 +1239,7 @@ dmu_send_obj(const char *pool, uint64_t tosnap, uint64_t fromsnap, } if (fromsnap != 0) { - zfs_bookmark_phys_t zb; + zfs_bookmark_phys_t zb = { 0 }; boolean_t is_clone; err = dsl_dataset_hold_obj(dp, fromsnap, FTAG, &fromds); @@ -1244,12 +1248,25 @@ dmu_send_obj(const char *pool, uint64_t tosnap, uint64_t fromsnap, dsl_pool_rele(dp, FTAG); return (err); } - if (!dsl_dataset_is_before(ds, fromds, 0)) - err = SET_ERROR(EXDEV); + if (!dsl_dataset_is_before(ds, fromds, 0)) { + err = SET_ERROR(ENODEV); + dsl_dataset_rele(fromds, FTAG); + dsl_dataset_rele_flags(ds, dsflags, FTAG); + dsl_pool_rele(dp, FTAG); + return (err); + } + zb.zbm_creation_time = dsl_dataset_phys(fromds)->ds_creation_time; zb.zbm_creation_txg = dsl_dataset_phys(fromds)->ds_creation_txg; zb.zbm_guid = dsl_dataset_phys(fromds)->ds_guid; + + if (dsl_dataset_is_zapified(fromds)) { + (void) zap_lookup(dp->dp_meta_objset, + fromds->ds_object, DS_FIELD_IVSET_GUID, 8, 1, + &zb.zbm_ivset_guid); + } + is_clone = (fromds->ds_dir != ds->ds_dir); dsl_dataset_rele(fromds, FTAG); err = dmu_send_impl(FTAG, dp, ds, &zb, is_clone, @@ -1299,7 +1316,7 @@ dmu_send(const char *tosnap, const char *fromsnap, boolean_t embedok, } if (fromsnap != NULL) { - zfs_bookmark_phys_t zb; + zfs_bookmark_phys_t zb = { 0 }; boolean_t is_clone = B_FALSE; int fsnamelen = strchr(tosnap, '@') - tosnap; @@ -1325,6 +1342,13 @@ dmu_send(const char *tosnap, const char *fromsnap, boolean_t embedok, dsl_dataset_phys(fromds)->ds_creation_txg; zb.zbm_guid = dsl_dataset_phys(fromds)->ds_guid; is_clone = (ds->ds_dir != fromds->ds_dir); + + if (dsl_dataset_is_zapified(fromds)) { + (void) zap_lookup(dp->dp_meta_objset, + fromds->ds_object, + DS_FIELD_IVSET_GUID, 8, 1, + &zb.zbm_ivset_guid); + } dsl_dataset_rele(fromds, FTAG); } } else { diff --git a/module/zfs/dsl_bookmark.c b/module/zfs/dsl_bookmark.c index 0a58115341c7..c59fd166360f 100644 --- a/module/zfs/dsl_bookmark.c +++ b/module/zfs/dsl_bookmark.c @@ -70,6 +70,12 @@ dsl_dataset_bmark_lookup(dsl_dataset_t *ds, const char *shortname, if (dsl_dataset_phys(ds)->ds_flags & DS_FLAG_CI_DATASET) mt = MT_NORMALIZE; + /* + * Zero out the bookmark in case the one stored on disk + * is in an older, shorter format. + */ + bzero(bmark_phys, sizeof (*bmark_phys)); + err = zap_lookup_norm(mos, bmark_zapobj, shortname, sizeof (uint64_t), sizeof (*bmark_phys) / sizeof (uint64_t), bmark_phys, mt, NULL, 0, NULL); @@ -188,8 +194,9 @@ dsl_bookmark_create_sync(void *arg, dmu_tx_t *tx) for (nvpair_t *pair = nvlist_next_nvpair(dbca->dbca_bmarks, NULL); pair != NULL; pair = nvlist_next_nvpair(dbca->dbca_bmarks, pair)) { dsl_dataset_t *snapds, *bmark_fs; - zfs_bookmark_phys_t bmark_phys; + zfs_bookmark_phys_t bmark_phys = { 0 }; char *shortname; + uint32_t bmark_len = BOOKMARK_PHYS_SIZE_V1; VERIFY0(dsl_dataset_hold(dp, fnvpair_value_string(pair), FTAG, &snapds)); @@ -214,10 +221,22 @@ dsl_bookmark_create_sync(void *arg, dmu_tx_t *tx) bmark_phys.zbm_creation_time = dsl_dataset_phys(snapds)->ds_creation_time; + if (snapds->ds_dir->dd_crypto_obj != 0) { + /* + * This value may not exist if the snapshot was + * created on a version of zfs before this field + * was added. + */ + int err = zap_lookup(mos, snapds->ds_object, + DS_FIELD_IVSET_GUID, sizeof (uint64_t), 1, + &bmark_phys.zbm_ivset_guid); + if (err == 0) + bmark_len = BOOKMARK_PHYS_SIZE_V2; + } + VERIFY0(zap_add(mos, bmark_fs->ds_bookmarks, shortname, sizeof (uint64_t), - sizeof (zfs_bookmark_phys_t) / sizeof (uint64_t), - &bmark_phys, tx)); + bmark_len / sizeof (uint64_t), &bmark_phys, tx)); spa_history_log_internal_ds(bmark_fs, "bookmark", tx, "name=%s creation_txg=%llu target_snap=%llu", diff --git a/module/zfs/dsl_crypt.c b/module/zfs/dsl_crypt.c index da2a126f2ebc..5bb6b55f22df 100644 --- a/module/zfs/dsl_crypt.c +++ b/module/zfs/dsl_crypt.c @@ -1963,21 +1963,23 @@ dsl_dataset_create_crypt_sync(uint64_t dsobj, dsl_dir_t *dd, typedef struct dsl_crypto_recv_key_arg { uint64_t dcrka_dsobj; + uint64_t dcrka_fromobj; dmu_objset_type_t dcrka_ostype; nvlist_t *dcrka_nvl; boolean_t dcrka_do_key; } dsl_crypto_recv_key_arg_t; static int -dsl_crypto_recv_raw_objset_check(dsl_dataset_t *ds, dmu_objset_type_t ostype, - nvlist_t *nvl, dmu_tx_t *tx) +dsl_crypto_recv_raw_objset_check(dsl_dataset_t *ds, dsl_dataset_t *fromds, + dmu_objset_type_t ostype, nvlist_t *nvl, dmu_tx_t *tx) { int ret; objset_t *os; dnode_t *mdn; uint8_t *buf = NULL; uint_t len; - uint64_t intval, nlevels, blksz, ibs, nblkptr, maxblkid; + uint64_t intval, nlevels, blksz, ibs; + uint64_t nblkptr, maxblkid, from_ivset_guid = 0; if (ostype != DMU_OST_ZFS && ostype != DMU_OST_ZVOL) return (SET_ERROR(EINVAL)); @@ -2044,6 +2046,24 @@ dsl_crypto_recv_raw_objset_check(dsl_dataset_t *ds, dmu_objset_type_t ostype, } rrw_exit(&ds->ds_bp_rwlock, FTAG); + /* + * Check that the ivset guid of the fromds matches the one from the + * send stream. Older versions of the encryption code did not have + * an ivset guid on the from dataset and did not send one in the + * stream. In this case we simply don't perform this check. + */ + intval = 0; + if (fromds != NULL) { + (void) nvlist_lookup_uint64(nvl, "from_ivset_guid", &intval); + (void) zap_lookup(tx->tx_pool->dp_meta_objset, + fromds->ds_object, DS_FIELD_IVSET_GUID, + sizeof (from_ivset_guid), 1, &from_ivset_guid); + + if (intval != 0 && from_ivset_guid != 0 && + intval != from_ivset_guid) + return (SET_ERROR(ZFS_ERR_FROM_IVSET_GUID_MISMATCH)); + } + return (0); } @@ -2063,7 +2083,11 @@ dsl_crypto_recv_raw_objset_sync(dsl_dataset_t *ds, dmu_objset_type_t ostype, VERIFY0(dmu_objset_from_ds(ds, &os)); mdn = DMU_META_DNODE(os); - /* fetch the values we need from the nvlist */ + /* + * Fetch the values we need from the nvlist. "to_ivset_guid" must + * be set on the snapshot, which doesn't exist yet. The receive + * code will take care of this for us later. + */ compress = fnvlist_lookup_uint64(nvl, "mdn_compress"); checksum = fnvlist_lookup_uint64(nvl, "mdn_checksum"); nlevels = fnvlist_lookup_uint64(nvl, "mdn_nlevels"); @@ -2127,7 +2151,7 @@ dsl_crypto_recv_raw_key_check(dsl_dataset_t *ds, nvlist_t *nvl, dmu_tx_t *tx) objset_t *mos = tx->tx_pool->dp_meta_objset; uint8_t *buf = NULL; uint_t len; - uint64_t intval, guid, version; + uint64_t intval, key_guid, version; boolean_t is_passphrase = B_FALSE; ASSERT(dsl_dataset_phys(ds)->ds_flags & DS_FLAG_INCONSISTENT); @@ -2152,10 +2176,10 @@ dsl_crypto_recv_raw_key_check(dsl_dataset_t *ds, nvlist_t *nvl, dmu_tx_t *tx) */ if (ds->ds_dir->dd_crypto_obj != 0) { ret = zap_lookup(mos, ds->ds_dir->dd_crypto_obj, - DSL_CRYPTO_KEY_GUID, 8, 1, &guid); + DSL_CRYPTO_KEY_GUID, 8, 1, &key_guid); if (ret != 0) return (ret); - if (intval != guid) + if (intval != key_guid) return (SET_ERROR(EACCES)); } @@ -2221,13 +2245,13 @@ dsl_crypto_recv_raw_key_sync(dsl_dataset_t *ds, nvlist_t *nvl, dmu_tx_t *tx) uint_t len; uint64_t rddobj, one = 1; uint8_t *keydata, *hmac_keydata, *iv, *mac; - uint64_t crypt, guid, keyformat, iters, salt; + uint64_t crypt, key_guid, keyformat, iters, salt; uint64_t version = ZIO_CRYPT_KEY_CURRENT_VERSION; char *keylocation = "prompt"; /* lookup the values we need to create the DSL Crypto Key */ crypt = fnvlist_lookup_uint64(nvl, DSL_CRYPTO_KEY_CRYPTO_SUITE); - guid = fnvlist_lookup_uint64(nvl, DSL_CRYPTO_KEY_GUID); + key_guid = fnvlist_lookup_uint64(nvl, DSL_CRYPTO_KEY_GUID); keyformat = fnvlist_lookup_uint64(nvl, zfs_prop_to_name(ZFS_PROP_KEYFORMAT)); iters = fnvlist_lookup_uint64(nvl, @@ -2282,7 +2306,7 @@ dsl_crypto_recv_raw_key_sync(dsl_dataset_t *ds, nvlist_t *nvl, dmu_tx_t *tx) /* sync the key data to the ZAP object on disk */ dsl_crypto_key_sync_impl(mos, dd->dd_crypto_obj, crypt, - rddobj, guid, iv, mac, keydata, hmac_keydata, keyformat, salt, + rddobj, key_guid, iv, mac, keydata, hmac_keydata, keyformat, salt, iters, tx); } @@ -2291,14 +2315,21 @@ dsl_crypto_recv_key_check(void *arg, dmu_tx_t *tx) { int ret; dsl_crypto_recv_key_arg_t *dcrka = arg; - dsl_dataset_t *ds = NULL; + dsl_dataset_t *ds = NULL, *fromds = NULL; ret = dsl_dataset_hold_obj(tx->tx_pool, dcrka->dcrka_dsobj, FTAG, &ds); if (ret != 0) goto error; - ret = dsl_crypto_recv_raw_objset_check(ds, + if (dcrka->dcrka_fromobj != 0) { + ret = dsl_dataset_hold_obj(tx->tx_pool, dcrka->dcrka_fromobj, + FTAG, &fromds); + if (ret != 0) + goto error; + } + + ret = dsl_crypto_recv_raw_objset_check(ds, fromds, dcrka->dcrka_ostype, dcrka->dcrka_nvl, tx); if (ret != 0) goto error; @@ -2313,11 +2344,16 @@ dsl_crypto_recv_key_check(void *arg, dmu_tx_t *tx) goto error; dsl_dataset_rele(ds, FTAG); + if (fromds != NULL) + dsl_dataset_rele(fromds, FTAG); + return (0); error: if (ds != NULL) dsl_dataset_rele(ds, FTAG); + if (fromds != NULL) + dsl_dataset_rele(fromds, FTAG); return (ret); } @@ -2342,12 +2378,13 @@ dsl_crypto_recv_key_sync(void *arg, dmu_tx_t *tx) * without wrapping it. */ int -dsl_crypto_recv_raw(const char *poolname, uint64_t dsobj, +dsl_crypto_recv_raw(const char *poolname, uint64_t dsobj, uint64_t fromobj, dmu_objset_type_t ostype, nvlist_t *nvl, boolean_t do_key) { dsl_crypto_recv_key_arg_t dcrka; dcrka.dcrka_dsobj = dsobj; + dcrka.dcrka_fromobj = fromobj; dcrka.dcrka_ostype = ostype; dcrka.dcrka_nvl = nvl; dcrka.dcrka_do_key = do_key; @@ -2357,7 +2394,8 @@ dsl_crypto_recv_raw(const char *poolname, uint64_t dsobj, } int -dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, nvlist_t **nvl_out) +dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, uint64_t from_ivset_guid, + nvlist_t **nvl_out) { int ret; objset_t *os; @@ -2368,8 +2406,9 @@ dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, nvlist_t **nvl_out) dsl_dir_t *rdd = NULL; dsl_pool_t *dp = ds->ds_dir->dd_pool; objset_t *mos = dp->dp_meta_objset; - uint64_t crypt = 0, guid = 0, format = 0; + uint64_t crypt = 0, key_guid = 0, format = 0; uint64_t iters = 0, salt = 0, version = 0; + uint64_t to_ivset_guid = 0; uint8_t raw_keydata[MASTER_KEY_MAX_LEN]; uint8_t raw_hmac_keydata[SHA512_HMAC_KEYLEN]; uint8_t iv[WRAPPING_IV_LEN]; @@ -2390,7 +2429,7 @@ dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, nvlist_t **nvl_out) if (ret != 0) goto error; - ret = zap_lookup(mos, dckobj, DSL_CRYPTO_KEY_GUID, 8, 1, &guid); + ret = zap_lookup(mos, dckobj, DSL_CRYPTO_KEY_GUID, 8, 1, &key_guid); if (ret != 0) goto error; @@ -2414,6 +2453,11 @@ dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, nvlist_t **nvl_out) if (ret != 0) goto error; + ret = zap_lookup(mos, ds->ds_object, DS_FIELD_IVSET_GUID, 8, 1, + &to_ivset_guid); + if (ret != 0) + goto error; + /* * We don't support raw sends of legacy on-disk formats. See the * comment in dsl_crypto_recv_key_check() for details. @@ -2463,7 +2507,7 @@ dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, nvlist_t **nvl_out) dsl_pool_config_exit(dp, FTAG); fnvlist_add_uint64(nvl, DSL_CRYPTO_KEY_CRYPTO_SUITE, crypt); - fnvlist_add_uint64(nvl, DSL_CRYPTO_KEY_GUID, guid); + fnvlist_add_uint64(nvl, DSL_CRYPTO_KEY_GUID, key_guid); fnvlist_add_uint64(nvl, DSL_CRYPTO_KEY_VERSION, version); VERIFY0(nvlist_add_uint8_array(nvl, DSL_CRYPTO_KEY_MASTER_KEY, raw_keydata, MASTER_KEY_MAX_LEN)); @@ -2485,6 +2529,8 @@ dsl_crypto_populate_key_nvlist(dsl_dataset_t *ds, nvlist_t **nvl_out) fnvlist_add_uint64(nvl, "mdn_indblkshift", mdn->dn_indblkshift); fnvlist_add_uint64(nvl, "mdn_nblkptr", mdn->dn_nblkptr); fnvlist_add_uint64(nvl, "mdn_maxblkid", mdn->dn_maxblkid); + fnvlist_add_uint64(nvl, "to_ivset_guid", to_ivset_guid); + fnvlist_add_uint64(nvl, "from_ivset_guid", from_ivset_guid); *nvl_out = nvl; return (0); @@ -2595,6 +2641,10 @@ dsl_dataset_crypt_stats(dsl_dataset_t *ds, nvlist_t *nv) zfs_prop_to_name(ZFS_PROP_PBKDF2_ITERS), 8, 1, &intval) == 0) { dsl_prop_nvlist_add_uint64(nv, ZFS_PROP_PBKDF2_ITERS, intval); } + if (zap_lookup(dd->dd_pool->dp_meta_objset, ds->ds_object, + DS_FIELD_IVSET_GUID, 8, 1, &intval) == 0) { + dsl_prop_nvlist_add_uint64(nv, ZFS_PROP_IVSET_GUID, intval); + } if (dsl_dir_get_encryption_root_ddobj(dd, &intval) == 0) { VERIFY0(dsl_dir_hold_obj(dd->dd_pool, intval, NULL, FTAG, diff --git a/module/zfs/dsl_dataset.c b/module/zfs/dsl_dataset.c index 168aea861637..473abafb12f3 100644 --- a/module/zfs/dsl_dataset.c +++ b/module/zfs/dsl_dataset.c @@ -1702,6 +1702,18 @@ dsl_dataset_snapshot_sync_impl(dsl_dataset_t *ds, const char *snapname, sizeof (remap_deadlist_obj), 1, &remap_deadlist_obj, tx)); } + /* + * Create a ivset guid for this snapshot if the dataset is + * encrypted. This may be overridden by a raw receive. + */ + if (ds->ds_dir->dd_crypto_obj != 0) { + uint64_t ivset_guid = unique_create(); + + dmu_object_zapify(mos, dsobj, DMU_OT_DSL_DATASET, tx); + VERIFY0(zap_add(mos, dsobj, DS_FIELD_IVSET_GUID, + sizeof (ivset_guid), 1, &ivset_guid, tx)); + } + ASSERT3U(dsl_dataset_phys(ds)->ds_prev_snap_txg, <, tx->tx_txg); dsl_dataset_phys(ds)->ds_prev_snap_obj = dsobj; dsl_dataset_phys(ds)->ds_prev_snap_txg = crtxg; diff --git a/module/zfs/zcp_get.c b/module/zfs/zcp_get.c index 2481bb1fe230..73d222e77697 100644 --- a/module/zfs/zcp_get.c +++ b/module/zfs/zcp_get.c @@ -411,6 +411,11 @@ get_special_prop(lua_State *state, dsl_dataset_t *ds, const char *dsname, case ZFS_PROP_INCONSISTENT: numval = dsl_get_inconsistent(ds); break; + case ZFS_PROP_IVSET_GUID: + error = zap_lookup(ds->ds_dir->dd_pool->dp_meta_objset, + ds->ds_object, DS_FIELD_IVSET_GUID, sizeof (numval), + 1, &numval); + break; case ZFS_PROP_RECEIVE_RESUME_TOKEN: { char *token = get_receive_resume_stats_impl(ds); diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index 0b41d087b2db..d0256ab51fec 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -792,7 +792,7 @@ tests = ['rsend_001_pos', 'rsend_002_pos', 'rsend_003_pos', 'rsend_004_pos', 'send-c_embedded_blocks', 'send-c_resume', 'send-cpL_varied_recsize', 'send-c_recv_dedup', 'send_encrypted_files', 'send_encrypted_heirarchy', 'send_encrypted_props', 'send_freeobjects', 'send_realloc_dnode_size', - 'send_hole_birth', 'send-wDR_encrypted_zvol'] + 'send_hole_birth', 'send_mixed_raw', 'send-wDR_encrypted_zvol'] tags = ['functional', 'rsend'] [tests/functional/scrub_mirror] diff --git a/tests/zfs-tests/tests/functional/rsend/Makefile.am b/tests/zfs-tests/tests/functional/rsend/Makefile.am index 316bcb4e6774..20d9770a31dd 100644 --- a/tests/zfs-tests/tests/functional/rsend/Makefile.am +++ b/tests/zfs-tests/tests/functional/rsend/Makefile.am @@ -42,6 +42,7 @@ dist_pkgdata_SCRIPTS = \ send_freeobjects.ksh \ send_realloc_dnode_size.ksh \ send_hole_birth.ksh \ + send_mixed_raw.ksh \ send-wDR_encrypted_zvol.ksh dist_pkgdata_DATA = \ diff --git a/tests/zfs-tests/tests/functional/rsend/send_mixed_raw.ksh b/tests/zfs-tests/tests/functional/rsend/send_mixed_raw.ksh new file mode 100755 index 000000000000..eea535af1100 --- /dev/null +++ b/tests/zfs-tests/tests/functional/rsend/send_mixed_raw.ksh @@ -0,0 +1,118 @@ +#!/bin/ksh -p +# +# 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) 2019 Datto, Inc. All rights reserved. +# + +. $STF_SUITE/include/libtest.shlib + +# +# DESCRIPTION: +# Verify that 'zfs receive' produces an error when mixing +# raw and non-raw sends in a way that would break IV set +# consistency. +# +# STRATEGY: +# 1. Create an initial dataset with 3 snapshots. +# 2. Perform a raw send of the first snapshot to 2 other datasets. +# 3. Perform a non-raw send of the second snapshot to one of +# the other datasets. Perform a raw send from this dataset to +# the last one. +# 4. Attempt to raw send the final snapshot of the first dataset +# to the other 2 datasets, which should fail. +# 5. Repeat steps 1-4, but using bookmarks for incremental sends. +# +# +# A B C notes +# ------------------------------------------------------------------------------ +# snap1 ---raw---> snap1 --raw--> snap1 # all snaps initialized via raw send +# snap2 -non-raw-> snap2 --raw--> snap2 # A sends non-raw to B, B sends raw to C +# snap3 ------------raw---------> snap3 # attempt send to C (should fail) +# + + +verify_runnable "both" + +function cleanup +{ + datasetexists $TESTPOOL/$TESTFS3 && \ + log_must zfs destroy -r $TESTPOOL/$TESTFS3 + datasetexists $TESTPOOL/$TESTFS2 && \ + log_must zfs destroy -r $TESTPOOL/$TESTFS2 + datasetexists $TESTPOOL/$TESTFS1 && \ + log_must zfs destroy -r $TESTPOOL/$TESTFS1 +} +log_onexit cleanup + +log_assert "Mixing raw and non-raw receives should fail" + +typeset passphrase="password" + +log_must eval "echo $passphrase | zfs create -o encryption=on" \ + "-o keyformat=passphrase $TESTPOOL/$TESTFS1" + +log_must zfs snapshot $TESTPOOL/$TESTFS1@1 +log_must touch /$TESTPOOL/$TESTFS1/a +log_must zfs snapshot $TESTPOOL/$TESTFS1@2 +log_must touch /$TESTPOOL/$TESTFS1/b +log_must zfs snapshot $TESTPOOL/$TESTFS1@3 + +# Testing with snapshots +log_must eval "zfs send -w $TESTPOOL/$TESTFS1@1 |" \ + "zfs receive $TESTPOOL/$TESTFS2" +log_must eval "echo $passphrase | zfs load-key $TESTPOOL/$TESTFS2" +log_must eval "zfs send -w $TESTPOOL/$TESTFS2@1 |" \ + "zfs receive $TESTPOOL/$TESTFS3" +log_must eval "echo $passphrase | zfs load-key $TESTPOOL/$TESTFS3" + +log_must eval "zfs send -i $TESTPOOL/$TESTFS1@1 $TESTPOOL/$TESTFS1@2 |" \ + "zfs receive $TESTPOOL/$TESTFS2" +log_must eval "zfs send -w -i $TESTPOOL/$TESTFS2@1 $TESTPOOL/$TESTFS2@2 |" \ + "zfs receive $TESTPOOL/$TESTFS3" + +log_mustnot eval "zfs send -w -i $TESTPOOL/$TESTFS1@2 $TESTPOOL/$TESTFS1@3 |" \ + "zfs receive $TESTPOOL/$TESTFS2" +log_mustnot eval "zfs send -w -i $TESTPOOL/$TESTFS2@2 $TESTPOOL/$TESTFS2@3 |" \ + "zfs receive $TESTPOOL/$TESTFS3" + +log_must zfs destroy -r $TESTPOOL/$TESTFS3 +log_must zfs destroy -r $TESTPOOL/$TESTFS2 + +# Testing with bookmarks +log_must zfs bookmark $TESTPOOL/$TESTFS1@1 $TESTPOOL/$TESTFS1#b1 +log_must zfs bookmark $TESTPOOL/$TESTFS1@2 $TESTPOOL/$TESTFS1#b2 + +log_must eval "zfs send -w $TESTPOOL/$TESTFS1@1 |" \ + "zfs receive $TESTPOOL/$TESTFS2" +log_must eval "echo $passphrase | zfs load-key $TESTPOOL/$TESTFS2" + +log_must zfs bookmark $TESTPOOL/$TESTFS2@1 $TESTPOOL/$TESTFS2#b1 + +log_must eval "zfs send -w $TESTPOOL/$TESTFS2@1 |" \ + "zfs receive $TESTPOOL/$TESTFS3" +log_must eval "echo $passphrase | zfs load-key $TESTPOOL/$TESTFS3" + +log_must eval "zfs send -i $TESTPOOL/$TESTFS1#b1 $TESTPOOL/$TESTFS1@2 |" \ + "zfs receive $TESTPOOL/$TESTFS2" +log_must eval "zfs send -w -i $TESTPOOL/$TESTFS2#b1 $TESTPOOL/$TESTFS2@2 |" \ + "zfs receive $TESTPOOL/$TESTFS3" + +log_mustnot eval "zfs send -w -i $TESTPOOL/$TESTFS1#b2" \ + "$TESTPOOL/$TESTFS1@3 | zfs receive $TESTPOOL/$TESTFS2" +log_mustnot eval "zfs send -w -i $TESTPOOL/$TESTFS2#b2" \ + "$TESTPOOL/$TESTFS2@3 | zfs receive $TESTPOOL/$TESTFS3" + +log_pass "Mixing raw and non-raw receives fail as expected"