Harden recv record validation

Refactors and enhances how we do recv record validation. I've
made sure to keep all existing checks and added a few new ones.
The per-record type validator functions are also used by zstream
so this is now common code.  Additionally, more detailed error
messages have been plumbed through to user space.

Reviewed-by: Brian Behlendorf <[email protected]>
Reviewed-by: Garth Snyder <[email protected]>
Signed-off-by: Alek Pinchuk <[email protected]>
Closes #18725
This commit is contained in:
Alek P authored and GitHub committed 2026-07-17 11:17:43 -07:00
1 parent 817fc376f1
commit 9b7642df93
13 files changed
+1398 -145

No files matched your search

+68
View File
@@ -292,6 +292,73 @@ parallel_compress_writes(compression_spec_t *target)
return (step);
}
/*
* Keep DRR_BEGIN feature flags consistent with the WRITE payloads we emit.
* Compressed WRITEs require DMU_BACKUP_FEATURE_COMPRESSED (and LZ4/ZSTD as
* appropriate). Recompressing to off clears COMPRESSED for non-raw streams;
* raw streams may retain encrypted compressed WRITEs that recompress skips.
* LZ4/ZSTD are cleared only when neither EMBED_DATA nor RAW remains, since
* recompress does not rewrite those record types.
*/
static disposition_t
chain_update_compress_features(void *item_in, void *context_in)
{
drr_packet_t *item = (drr_packet_t *)item_in;
compression_spec_t *spec = (compression_spec_t *)context_in;
struct drr_begin *drrb;
uint64_t flags;
if (item == NULL)
return (D_OK);
if (item->dp_drr.drr_type != DRR_BEGIN)
return (D_OK);
drrb = &item->dp_drr.drr_u.drr_begin;
flags = DMU_GET_FEATUREFLAGS(drrb->drr_versioninfo);
if (ctype_is_uncompressed(spec->cs_type)) {
if (!(flags & DMU_BACKUP_FEATURE_RAW))
flags &= ~DMU_BACKUP_FEATURE_COMPRESSED;
if ((flags & (DMU_BACKUP_FEATURE_EMBED_DATA |
DMU_BACKUP_FEATURE_RAW)) == 0) {
flags &= ~(DMU_BACKUP_FEATURE_LZ4 |
DMU_BACKUP_FEATURE_ZSTD);
}
} else {
flags |= DMU_BACKUP_FEATURE_COMPRESSED;
if (spec->cs_type == ZIO_COMPRESS_ZSTD) {
flags |= DMU_BACKUP_FEATURE_ZSTD;
} else if (spec->cs_type >= ZIO_COMPRESS_LZ4) {
flags |= DMU_BACKUP_FEATURE_LZ4;
}
}
DMU_SET_FEATUREFLAGS(drrb->drr_versioninfo, flags);
return (D_OK);
}
static chain_step_t
serial_update_compress_features(compression_spec_t *target)
{
int this_spec = next_spec++ % MAX_COMPRESSION_STEPS;
compression_spec_t *context = &specs[this_spec];
VERIFY3P(target, !=, NULL);
*context = *target;
chain_step_t step = {
.cs_type = CS_SERIAL,
.cs_in_size = sizeof (drr_packet_t),
.cs_out_size = sizeof (drr_packet_t),
.cs_context = context,
.cs_serial = {
.process = chain_update_compress_features,
}
};
return (step);
}
int
zstream_do_recompress(int argc, char *argv[])
{
@@ -350,6 +417,7 @@ zstream_do_recompress(int argc, char *argv[])
STANDARD_INPUT_STACK(NULL),
parallel_decompress_writes(&spec),
parallel_compress_writes(&spec),
serial_update_compress_features(&spec),
STANDARD_OUTPUT_STACK(NULL)
};
+79 -23
View File
@@ -21,27 +21,48 @@
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/dmu_recv.h>
#include <sys/stdtypes.h>
#include <sys/zfs_ioctl.h>
#include <sys/zio_compress.h>
#include "zstream_modules.h"
/*
* Validate consistency and well-formedness of the actual DRR records. I
* have swept all the existing validation code into this module, but it's
* still pretty sparse.
* Validate consistency and well-formedness of the actual DRR records.
*/
#define MAX_VALIDATIONS 4
typedef struct {
int nesting;
uint64_t featureflags;
boolean_t begin_spill;
} validate_context_t;
static validate_context_t contexts[MAX_VALIDATIONS];
static int next_context = 0;
static void
validate_fail(int err, const char *msg)
{
if (err == 0)
return;
if (msg != NULL && msg[0] != '\0')
errx(1, "%s", msg);
errx(1, "invalid receive stream record (error %d)", err);
}
static boolean_t
validate_stream_has_feature(const validate_context_t *context, uint64_t feature)
{
/*
* STREAM_HAS_FEATURE() describes the first DRR_BEGIN in the input.
* Recursive streams can contain later BEGIN records with different
* feature flags, so validation must use the current substream flags.
*/
return ((context->featureflags & feature) != 0);
}
static disposition_t
chain_validate_records(void *item_in, void *context_in)
{
@@ -54,6 +75,14 @@ chain_validate_records(void *item_in, void *context_in)
struct dmu_replay_record *drr = &item->dp_drr;
struct drr_write *drrw = &drr->drr_u.drr_write;
struct drr_object *drro = &drr->drr_u.drr_object;
struct drr_spill *drrs = &drr->drr_u.drr_spill;
struct drr_write_embedded *drrwe = &drr->drr_u.drr_write_embedded;
struct drr_free *drrf = &drr->drr_u.drr_free;
struct drr_freeobjects *drrfo = &drr->drr_u.drr_freeobjects;
struct drr_object_range *drror = &drr->drr_u.drr_object_range;
char errbuf[RECV_CHECK_ERRBUFLEN];
int err;
boolean_t is_raw;
if (OPTION_ENABLED(CA_DO_NOT_VALIDATE))
return (D_OK);
@@ -65,6 +94,10 @@ chain_validate_records(void *item_in, void *context_in)
if (drr->drr_type == DRR_BEGIN) {
VERIFY0(context->nesting);
context->nesting++;
context->featureflags = DMU_GET_FEATUREFLAGS(
drr->drr_u.drr_begin.drr_versioninfo);
context->begin_spill = !!(drr->drr_u.drr_begin.drr_flags &
DRR_FLAG_SPILL_BLOCK);
} else if (drr->drr_type == DRR_END) {
VERIFY3S(context->nesting, >=, 0);
if (context->nesting > 0)
@@ -75,6 +108,8 @@ chain_validate_records(void *item_in, void *context_in)
VERIFY3S(context->nesting, ==, 1);
}
is_raw = validate_stream_has_feature(context, DMU_BACKUP_FEATURE_RAW);
switch (drr->drr_type) {
case DRR_BEGIN:
@@ -82,27 +117,46 @@ chain_validate_records(void *item_in, void *context_in)
break;
case DRR_OBJECT:
{
boolean_t is_raw = !!(chain_attrs->ca_feature_flags &
DMU_BACKUP_FEATURE_RAW);
boolean_t bonus_gt_raw = drro->drr_bonuslen >
drro->drr_raw_bonuslen;
if (is_raw && bonus_gt_raw) {
fprintf(stderr,
"Warning: object %llu has bonuslen = "
"%u > raw_bonuslen = %u\n\n",
(u_longlong_t)drro->drr_object,
drro->drr_bonuslen,
drro->drr_raw_bonuslen);
}
break;
}
err = recv_check_drr_object(drro, NULL, is_raw,
context->begin_spill, context->featureflags, errbuf,
sizeof (errbuf));
validate_fail(err, errbuf);
break;
case DRR_WRITE:
if (drrw->drr_compressiontype >= ZIO_COMPRESS_FUNCTIONS) {
errx(1, "invalid compression type: %d",
drrw->drr_compressiontype);
}
err = recv_check_drr_write(drrw, NULL, is_raw,
context->featureflags, errbuf, sizeof (errbuf));
validate_fail(err, errbuf);
break;
case DRR_WRITE_EMBEDDED:
err = recv_check_drr_write_embedded(drrwe, NULL, is_raw,
context->featureflags, errbuf, sizeof (errbuf));
validate_fail(err, errbuf);
break;
case DRR_SPILL:
err = recv_check_drr_spill(drrs, NULL, is_raw,
context->featureflags, errbuf, sizeof (errbuf));
validate_fail(err, errbuf);
break;
case DRR_FREE:
case DRR_REDACT:
err = recv_check_drr_free(drrf, errbuf, sizeof (errbuf));
validate_fail(err, errbuf);
break;
case DRR_FREEOBJECTS:
err = recv_check_drr_freeobjects(drrfo, errbuf,
sizeof (errbuf));
validate_fail(err, errbuf);
break;
case DRR_OBJECT_RANGE:
err = recv_check_drr_object_range(drror, is_raw, errbuf,
sizeof (errbuf));
validate_fail(err, errbuf);
break;
default:
@@ -118,6 +172,8 @@ serial_validate_records(void)
int context_ix = next_context++ % MAX_VALIDATIONS;
validate_context_t *context = &contexts[context_ix];
context->nesting = 0;
context->featureflags = 0;
context->begin_spill = B_FALSE;
chain_step_t step = {
.cs_type = CS_SERIAL,
+30
View File
@@ -32,10 +32,12 @@
#define _DMU_RECV_H
#include <sys/inttypes.h>
#include <sys/types.h>
#include <sys/dsl_bookmark.h>
#include <sys/dsl_dataset.h>
#include <sys/spa.h>
#include <sys/objlist.h>
#include <sys/zfs_ioctl.h>
extern const char *const recv_clone_name;
@@ -61,6 +63,7 @@ typedef struct dmu_recv_cookie {
void *drc_owner;
cred_t *drc_cred;
nvlist_t *drc_begin_nvl;
nvlist_t *drc_errors;
objset_t *drc_os;
zfs_file_t *drc_fp; /* The file to read the stream from */
@@ -86,4 +89,31 @@ int dmu_recv_stream(dmu_recv_cookie_t *, offset_t *);
int dmu_recv_end(dmu_recv_cookie_t *, void *);
boolean_t dmu_objset_is_receiving(objset_t *);
/*
* Receive stream record validators. spa may be NULL to validate against the
* largest supported pool limits (for userland tools such as zstream). errbuf
* is optional; when provided it receives a short description on failure.
*
* Size fields that exceed a pool or on-wire maximum return ERANGE; other
* malformed or inconsistent records return EINVAL. Callers of lzc_receive*
* may therefore observe ERANGE where older OpenZFS modules returned EINVAL
* for the same oversized record.
*/
#define RECV_CHECK_ERRBUFLEN 256
int recv_check_drr_object(const struct drr_object *, spa_t *, boolean_t raw,
boolean_t spill, uint64_t featureflags, char *errbuf, size_t errbuflen);
int recv_check_drr_free(const struct drr_free *, char *errbuf,
size_t errbuflen);
int recv_check_drr_freeobjects(const struct drr_freeobjects *, char *errbuf,
size_t errbuflen);
int recv_check_drr_object_range(const struct drr_object_range *, boolean_t raw,
char *errbuf, size_t errbuflen);
int recv_check_drr_spill(const struct drr_spill *, spa_t *, boolean_t raw,
uint64_t featureflags, char *errbuf, size_t errbuflen);
int recv_check_drr_write(const struct drr_write *, spa_t *, boolean_t raw,
uint64_t featureflags, char *errbuf, size_t errbuflen);
int recv_check_drr_write_embedded(const struct drr_write_embedded *, spa_t *,
boolean_t raw, uint64_t featureflags, char *errbuf, size_t errbuflen);
#endif /* _DMU_RECV_H */
+1
View File
@@ -381,6 +381,7 @@ typedef enum {
#define ZPROP_SOURCE_VAL_RECVD "$recvd"
#define ZPROP_N_MORE_ERRORS "N_MORE_ERRORS"
#define ZFS_RECV_ERR_STREAM "stream"
/*
* Dataset flag implemented as a special entry in the props zap object
+25 -7
View File
@@ -5099,16 +5099,24 @@ zfs_receive_one(libzfs_handle_t *hdl, int infd, const char *tosnap,
char tbuf[1024];
zfs_prop_t prop;
int intval;
const char *pname = nvpair_name(prop_err);
prop = zfs_name_to_prop(nvpair_name(prop_err));
/*
* errors may also carry non-property keys (e.g.
* ZFS_RECV_ERR_STREAM). Only int32 pairs are
* property apply failures.
*/
if (nvpair_type(prop_err) != DATA_TYPE_INT32)
continue;
prop = zfs_name_to_prop(pname);
(void) nvpair_value_int32(prop_err, &intval);
if (strcmp(nvpair_name(prop_err),
ZPROP_N_MORE_ERRORS) == 0) {
if (strcmp(pname, ZPROP_N_MORE_ERRORS) == 0) {
trunc_prop_errs(intval);
break;
} else if (snapname == NULL || finalsnap == NULL ||
strcmp(finalsnap, snapname) == 0 ||
strcmp(nvpair_name(prop_err),
strcmp(pname,
zfs_prop_to_name(ZFS_PROP_REFQUOTA)) != 0) {
/*
* Skip the special case of, for example,
@@ -5124,7 +5132,7 @@ zfs_receive_one(libzfs_handle_t *hdl, int infd, const char *tosnap,
(void) snprintf(tbuf, sizeof (tbuf),
dgettext(TEXT_DOMAIN,
"cannot receive %s property on %s"),
nvpair_name(prop_err), name);
pname, name);
zfs_setprop_error(hdl, prop, intval, tbuf);
}
}
@@ -5193,6 +5201,13 @@ zfs_receive_one(libzfs_handle_t *hdl, int infd, const char *tosnap,
}
if (ioctl_err != 0) {
const char *stream_err = NULL;
if (prop_errors != NULL) {
(void) nvlist_lookup_string(prop_errors,
ZFS_RECV_ERR_STREAM, &stream_err);
}
switch (ioctl_errno) {
case ENODEV:
cp = strchr(destsnap, '@');
@@ -5242,11 +5257,14 @@ zfs_receive_one(libzfs_handle_t *hdl, int infd, const char *tosnap,
*cp = '@';
break;
case EINVAL:
if (embedded && !raw) {
case ERANGE:
if (stream_err != NULL) {
zfs_error_aux(hdl, "%s", stream_err);
} else if (ioctl_errno == EINVAL && embedded && !raw) {
zfs_error_aux(hdl, dgettext(TEXT_DOMAIN,
"incompatible embedded data stream "
"feature with encrypted receive."));
} else if (flags->resumable) {
} else if (ioctl_errno == EINVAL && flags->resumable) {
zfs_error_aux(hdl, dgettext(TEXT_DOMAIN,
"kernel modules must be upgraded to "
"receive this stream."));
+35 -8
View File
@@ -1205,11 +1205,22 @@ recv_impl(const char *snapname, nvlist_t *recvdprops, nvlist_t *localprops,
error = nvlist_lookup_uint64(outnvl, "error_flags",
errflags);
if (error == 0 && errors != NULL) {
/*
* Copy the kernel "errors" nvlist when present. On success
* this carries property apply failures; on failure it may
* also include ZFS_RECV_ERR_STREAM. Do not allocate an
* empty nvlist when the key is absent — leave *errors
* unchanged (callers must initialize it to NULL). If the
* caller requested errors and the ioctl succeeded but the
* key is missing, treat that as ENOENT (incomplete output).
*/
if (errors != NULL && outnvl != NULL) {
nvlist_t *nvl;
error = nvlist_lookup_nvlist(outnvl, "errors", &nvl);
if (error == 0)
if (nvlist_lookup_nvlist(outnvl, "errors", &nvl) == 0)
*errors = fnvlist_dup(nvl);
else if (error == 0)
error = ENOENT;
}
fnvlist_free(innvl);
@@ -1261,11 +1272,19 @@ recv_impl(const char *snapname, nvlist_t *recvdprops, nvlist_t *localprops,
if (errflags != NULL)
*errflags = zc.zc_obj;
}
if (errors != NULL)
VERIFY0(nvlist_unpack(
(void *)(uintptr_t)zc.zc_nvlist_dst,
zc.zc_nvlist_dst_size, errors, KM_SLEEP));
/*
* Unpack errors when the kernel filled the dst buffer, on
* success or failure (stream/property details). The caller's
* pointer is unchanged when the buffer was not filled.
*/
if (errors != NULL && zc.zc_nvlist_dst_filled) {
if (nvlist_unpack((void *)(uintptr_t)zc.zc_nvlist_dst,
zc.zc_nvlist_dst_size, errors, KM_SLEEP) != 0 &&
error == 0) {
error = EINVAL;
}
}
if (rp_packed != NULL)
@@ -1352,7 +1371,15 @@ lzc_receive_with_header(const char *snapname, nvlist_t *props,
* The 'action_handle' and 'cleanup_fd' are no longer used, and are ignored.
*
* The 'errors' nvlist contains an entry for each unapplied received
* property. Callers are responsible for freeing this nvlist.
* property, and may also include ZFS_RECV_ERR_STREAM when the kernel
* rejected the stream. It is set when the kernel returned an errors
* payload (including on ioctl failure); otherwise the caller's pointer
* is unchanged and must be initialized to NULL. Callers are responsible
* for freeing this nvlist when it is set.
*
* Oversized stream records return ERANGE (libzfs maps this to
* EZFS_BADSTREAM along with EINVAL). Older OpenZFS modules often
* returned EINVAL for the same condition.
*/
int
lzc_receive_one(const char *snapname, nvlist_t *props,
+1019 -93
View File
File diff suppressed because it is too large. Load diff
+30 -12
View File
@@ -1632,17 +1632,25 @@ put_nvlist(zfs_cmd_t *zc, nvlist_t *nvl)
size = fnvlist_size(nvl);
if (size > zc->zc_nvlist_dst_size) {
error = SET_ERROR(ENOMEM);
} else {
packed = fnvlist_pack(nvl, &size);
if (ddi_copyout(packed, (void *)(uintptr_t)zc->zc_nvlist_dst,
size, zc->zc_iflags) != 0)
error = SET_ERROR(EFAULT);
fnvlist_pack_free(packed, size);
/*
* Report the required size so the caller can retry, but do
* not claim the destination buffer was filled.
*/
zc->zc_nvlist_dst_size = size;
zc->zc_nvlist_dst_filled = B_FALSE;
return (SET_ERROR(ENOMEM));
}
zc->zc_nvlist_dst_size = size;
zc->zc_nvlist_dst_filled = B_TRUE;
packed = fnvlist_pack(nvl, &size);
if (ddi_copyout(packed, (void *)(uintptr_t)zc->zc_nvlist_dst,
size, zc->zc_iflags) != 0) {
zc->zc_nvlist_dst_filled = B_FALSE;
error = SET_ERROR(EFAULT);
} else {
zc->zc_nvlist_dst_size = size;
zc->zc_nvlist_dst_filled = B_TRUE;
}
fnvlist_pack_free(packed, size);
return (error);
}
@@ -5765,6 +5773,7 @@ zfs_ioc_recv_impl(char *tofs, char *tosnap, const char *origin,
&off);
if (error != 0)
goto out;
drc.drc_errors = *errors;
tofs_was_redacted = dsl_get_redacted(drc.drc_ds);
/*
@@ -6120,7 +6129,9 @@ zfs_ioc_recv(zfs_cmd_t *zc)
/*
* Now that all props, initial and delayed, are set, report the prop
* errors to the caller.
* errors to the caller. Do not overwrite a non-zero receive errno
* (e.g. ERANGE/EINVAL from stream validation) if the errors nvlist
* cannot be copied out.
*/
if (zc->zc_nvlist_dst_size != 0 && errors != NULL &&
(nvlist_smush(errors, zc->zc_nvlist_dst_size) != 0 ||
@@ -6129,7 +6140,8 @@ zfs_ioc_recv(zfs_cmd_t *zc)
* Caller made zc->zc_nvlist_dst less than the minimum expected
* size or supplied an invalid address.
*/
error = SET_ERROR(EINVAL);
if (error == 0)
error = SET_ERROR(EINVAL);
}
out:
@@ -8764,9 +8776,15 @@ zfsdev_ioctl_common(uint_t vecnum, zfs_cmd_t *zc, int flag)
}
if (smusherror == 0)
puterror = put_nvlist(zc, outnvl);
else
puterror = smusherror;
}
if (puterror != 0)
/*
* Prefer the operation's errno over a copyout/smush failure
* so stream validation errors are not replaced by EINVAL.
*/
if (puterror != 0 && error == 0)
error = puterror;
nvlist_free(outnvl);
+3 -2
View File
@@ -1001,7 +1001,7 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos',
'rsend_021_pos', 'rsend_022_pos', 'rsend_024_pos', 'rsend_025_pos',
'rsend_026_neg', 'rsend_027_pos', 'rsend_028_neg', 'rsend_029_neg',
'rsend_030_pos', 'rsend_031_pos', 'rsend-exclude_001_pos',
'rsend-exclude_002_pos', 'send-c_verify_ratio',
'rsend-exclude_002_pos', 'recv_validate_001_neg', 'send-c_verify_ratio',
'send-c_verify_contents', 'send-c_props', 'send-c_incremental',
'send-c_volume',
'send-c_lz4_disabled', 'send-c_recv_lz4_disabled',
@@ -1163,7 +1163,8 @@ tests = ['zstream_checksum_001_pos',
'zstream_recompress_001_pos', 'zstream_recompress_002_pos',
'zstream_recompress_003_pos', 'zstream_recompress_004_pos',
'zstream_recompress_005_pos',
'zstream_redup_001_pos']
'zstream_redup_001_pos',
'zstream_validate_001_neg']
tags = ['functional', 'zstream']
[tests/functional/zvol/zvol_ENOSPC]
+3
View File
@@ -418,6 +418,7 @@ nobase_dist_datadir_zfs_tests_tests_DATA += \
functional/zstream/little-endian-all-drr-types-incr-XDR.zsend.bz2 \
functional/zstream/decompress.zsend.bz2 \
functional/zstream/decompress-crypt.zsend.bz2 \
functional/zstream/undersized-write-lsize.zsend.bz2 \
functional/zstream/big-endian-long-payloads.zsend.bz2 \
functional/zstream/little-endian-long-payloads.zsend.bz2 \
functional/zstream/beadtbn-new.dump.bz2 \
@@ -2108,6 +2109,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/rsend/cleanup.ksh \
functional/rsend/recv_dedup_encrypted_zvol.ksh \
functional/rsend/recv_dedup.ksh \
functional/rsend/recv_validate_001_neg.ksh \
functional/rsend/rsend_001_pos.ksh \
functional/rsend/rsend_002_pos.ksh \
functional/rsend/rsend_003_pos.ksh \
@@ -2439,6 +2441,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/zstream/zstream_recompress_004_pos.ksh \
functional/zstream/zstream_recompress_005_pos.ksh \
functional/zstream/zstream_redup_001_pos.ksh \
functional/zstream/zstream_validate_001_neg.ksh \
functional/zvol/zvol_cli/cleanup.ksh \
functional/zvol/zvol_cli/setup.ksh \
functional/zvol/zvol_cli/zvol_cli_001_pos.ksh \
@@ -0,0 +1,58 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# 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.
#
#
# Copyright (c) 2026 by ConnectWise. All rights reserved.
#
. $STF_SUITE/tests/functional/rsend/rsend.kshlib
#
# Description:
# Verify that zfs recv rejects a send stream whose first compressed
# DRR_WRITE has logical_size below SPA_MINBLOCKSIZE.
#
# Strategy:
# 1. Load the prebuilt mutated stream from the zstream testdata
# (undersized-write-lsize.zsend.bz2)
# 2. Confirm zfs recv fails with the shared recv_check
# "logical size ... below minimum" message
#
verify_runnable "both"
function cleanup
{
datasetexists $badrecv && destroy_dataset $badrecv -r
[[ -f $bad ]] && rm -f $bad
[[ -f $err ]] && rm -f $err
}
log_onexit cleanup
log_assert "undersized WRITE logical_size is rejected by zfs recv"
typeset src=$STF_SUITE/tests/functional/zstream/undersized-write-lsize.zsend.bz2
typeset bad=$BACKDIR/undersized-write-lsize.zsend
typeset err=$BACKDIR/undersized-write-lsize.err
typeset badrecv=$POOL/badrecv
log_must mkdir -p $BACKDIR
log_must eval "bzcat <$src >$bad"
datasetexists $badrecv && log_must zfs destroy -r $badrecv
log_mustnot eval "zfs recv $badrecv < $bad >$err 2>&1"
log_must eval "grep -E -q 'DRR_WRITE logical size .* below minimum' $err"
log_note "recv rejected: $(head -1 $err)"
datasetexists $badrecv && log_must zfs destroy -r $badrecv
log_pass "undersized WRITE logical_size is rejected by zfs recv"
@@ -0,0 +1,47 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# 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.
#
#
# Copyright (c) 2026 by ConnectWise. All rights reserved.
#
. $STF_SUITE/tests/functional/zstream/zstream.kshlib
#
# Description:
# Verify that zstream dump rejects a send stream whose first compressed
# DRR_WRITE has logical_size below SPA_MINBLOCKSIZE.
#
# Strategy:
# 1. Load the prebuilt mutated stream
# (undersized-write-lsize.zsend.bz2)
# 2. Confirm zstream dump fails with the shared recv_check
# "logical size ... below minimum" message
#
verify_runnable "both"
log_assert "undersized WRITE logical_size is rejected by zstream dump"
log_onexit cleanup_pool $POOL
typeset src="$ZSTREAM_DATADIR/undersized-write-lsize.zsend.bz2"
typeset bad=$BACKDIR/bad
typeset err=$BACKDIR/err
log_must eval "bzcat <$src >$bad"
log_mustnot eval "zstream dump $bad >$err 2>&1"
log_must eval "grep -E -q 'DRR_WRITE logical size .* below minimum' $err"
log_note "dump rejected: $(head -1 $err)"
log_pass "undersized WRITE logical_size is rejected by zstream dump"