mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-29 03:45:02 +01:00
Fix receive of split large blocks with a short trailing chunk
A dataset with a large recordsize can store a single-block file whose block size is not a power of two. When such a block is sent without large blocks (no -L), the sender splits it into SPA_OLD_MAXBLOCKSIZE (128K) chunks, and the final chunk is smaller than the block size. flush_write_batch_impl() already handles any WRITE record whose size differs from the object's block size by doing a normal dmu_write(), but it first asserted the record was always larger than the block size. The shorter trailing chunk violates that assertion, so receiving such a stream panicked the receive_writer thread on debug builds; production builds took the correct dmu_write() path and were unaffected. Drop the assertion and describe both size-mismatch cases in the comment; the dmu_write() path already handles a record smaller than the block size. Add an rsend test that sends such a block without -L (initial and incremental, plain and compressed) and verifies the received file matches. Reviewed-by: Brian Behlendorf <[email protected]> Signed-off-by: MorganaFuture <[email protected]> Issue #17829 Closes #18749
This commit is contained in:
1 parent
5bc4610728
commit
f217627d44
4 files changed
+161
-8
No files matched your search
+14
-7
@@ -2272,14 +2272,21 @@ flush_write_batch_impl(struct receive_writer_arg *rwa)
|
||||
|
||||
if (drrw->drr_logical_size != dn->dn_datablksz) {
|
||||
/*
|
||||
* The WRITE record is larger than the object's block
|
||||
* size. We must be receiving an incremental
|
||||
* large-block stream into a dataset that previously did
|
||||
* a non-large-block receive. Lightweight writes must
|
||||
* be exactly one block, so we need to decompress the
|
||||
* data (if compressed) and do a normal dmu_write().
|
||||
* The WRITE record size does not match the
|
||||
* object's block size. This happens when the
|
||||
* record is larger than the block size (an
|
||||
* incremental large-block stream received into a
|
||||
* dataset that previously did a non-large-block
|
||||
* receive), and also when it is a smaller trailing
|
||||
* chunk from splitting a large block for a stream
|
||||
* sent without large blocks: a single-block object
|
||||
* can have a non-power-of-2 block size, so its final
|
||||
* SPA_OLD_MAXBLOCKSIZE-sized chunk may be shorter
|
||||
* than the block size. Either way a lightweight
|
||||
* write is not possible (those must cover exactly
|
||||
* one block), so we decompress the data (if
|
||||
* compressed) and do a normal dmu_write().
|
||||
*/
|
||||
ASSERT3U(drrw->drr_logical_size, >, dn->dn_datablksz);
|
||||
if (DRR_WRITE_COMPRESSED(drrw)) {
|
||||
abd_t *decomp_abd =
|
||||
abd_alloc_linear(drrw->drr_logical_size,
|
||||
|
||||
@@ -1010,7 +1010,8 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos',
|
||||
'send_encrypted_freeobjects', 'send_encrypted_hierarchy',
|
||||
'send_encrypted_props', 'send_encrypted_truncated_files',
|
||||
'send_freeobjects', 'send_realloc_files', 'send_realloc_encrypted_files',
|
||||
'send_spill_block', 'send_holds', 'send_hole_birth', 'send_mixed_raw',
|
||||
'send_spill_block', 'send_split_large_block', 'send_holds',
|
||||
'send_hole_birth', 'send_mixed_raw',
|
||||
'send-wR_encrypted_zvol',
|
||||
'send_partial_dataset', 'send_invalid',
|
||||
'send_large_blocks_incremental', 'send_large_blocks_initial',
|
||||
|
||||
@@ -2171,6 +2171,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
|
||||
functional/rsend/send_realloc_encrypted_files.ksh \
|
||||
functional/rsend/send_realloc_files.ksh \
|
||||
functional/rsend/send_spill_block.ksh \
|
||||
functional/rsend/send_split_large_block.ksh \
|
||||
functional/rsend/send-wR_encrypted_zvol.ksh \
|
||||
functional/rsend/setup.ksh \
|
||||
functional/scrub_mirror/cleanup.ksh \
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/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 MorganaFuture. All rights reserved.
|
||||
#
|
||||
|
||||
. $STF_SUITE/include/libtest.shlib
|
||||
. $STF_SUITE/tests/functional/rsend/rsend.kshlib
|
||||
|
||||
#
|
||||
# Description:
|
||||
# A dataset with a large recordsize can hold a single-block file whose block
|
||||
# size is not a power of two. Sending it without large blocks (no -L) splits
|
||||
# that block into SPA_OLD_MAXBLOCKSIZE (128K) chunks, and the final chunk is
|
||||
# smaller than the block size. The receiver must accept such a stream, for
|
||||
# plain, compressed, initial and incremental sends, and reproduce the file
|
||||
# exactly. Splitting of multi-block objects and a follow-up large-block (-L)
|
||||
# incremental that rewrites the object are covered too.
|
||||
#
|
||||
# Previously the receiver assumed a size-mismatched WRITE record was always
|
||||
# larger than the block size, so this shorter trailing chunk tripped a bad
|
||||
# assertion and hung the receive_writer thread on debug builds (#17829).
|
||||
#
|
||||
|
||||
verify_runnable "both"
|
||||
|
||||
typeset SPLIT=131072 # SPA_OLD_MAXBLOCKSIZE, the no-large-block split size
|
||||
|
||||
function cleanup
|
||||
{
|
||||
rm -f $BACKDIR/fs@* $BACKDIR/fsmulti@* $BACKDIR/stream*
|
||||
for ds in fs fsmulti recv recvc recvmulti; do
|
||||
datasetexists $POOL/$ds && destroy_dataset $POOL/$ds "-rR"
|
||||
done
|
||||
}
|
||||
|
||||
#
|
||||
# Extract the logical_size of every WRITE record in a stream, one per line.
|
||||
#
|
||||
function write_sizes # stream
|
||||
{
|
||||
zstream dump -v "$1" | awk '/^WRITE object/ {
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i == "logical_size")
|
||||
print $(i + 2)
|
||||
}'
|
||||
}
|
||||
|
||||
#
|
||||
# Assert the stream split a large block into full 128K chunks plus a shorter
|
||||
# trailing chunk (the case that used to trip the assertion).
|
||||
#
|
||||
function assert_split_with_tail # stream
|
||||
{
|
||||
typeset full=0 tail=0 s
|
||||
for s in $(write_sizes "$1"); do
|
||||
if [[ $s -eq $SPLIT ]]; then
|
||||
full=$((full + 1))
|
||||
elif [[ $s -gt 0 && $s -lt $SPLIT ]]; then
|
||||
tail=$((tail + 1))
|
||||
fi
|
||||
done
|
||||
log_note "$1: 128K WRITEs=$full short-tail WRITEs=$tail"
|
||||
[[ $full -ge 1 ]] || log_fail "$1: expected full 128K WRITE records"
|
||||
[[ $tail -ge 1 ]] || log_fail "$1: expected a short trailing WRITE record"
|
||||
}
|
||||
|
||||
log_assert "A non-large-block send of a large non-power-of-2 block is received intact"
|
||||
log_onexit cleanup
|
||||
|
||||
log_must zfs create -o recordsize=1m $POOL/fs
|
||||
|
||||
# 300000 bytes in a recordsize=1m dataset is stored in a single block whose
|
||||
# size is rounded up to 300032 bytes (293K): larger than 128K and not a power
|
||||
# of two, so a send without -L must emit a short trailing 128K-split chunk.
|
||||
log_must dd if=/dev/urandom of=/$POOL/fs/file bs=1000 count=300
|
||||
log_must zfs snapshot $POOL/fs@snap1
|
||||
|
||||
# Guard the premise: were the block ever a power of two the split would be
|
||||
# even and would no longer exercise the short-tail path this test targets.
|
||||
typeset dblk=$(zdb -dddd $POOL/fs 2>/dev/null | awk '/ZFS plain file/ {print $4}')
|
||||
log_must test "$dblk" = "293K"
|
||||
|
||||
# Initial send without -L: verify via zstream dump that the block really was
|
||||
# split into 128K chunks with a shorter trailing chunk, then that the receive
|
||||
# reproduces the file byte-for-byte.
|
||||
log_must eval "zfs send $POOL/fs@snap1 >$BACKDIR/stream.snap1"
|
||||
assert_split_with_tail "$BACKDIR/stream.snap1"
|
||||
log_must eval "zfs recv $POOL/recv <$BACKDIR/stream.snap1"
|
||||
log_must diff /$POOL/fs/file /$POOL/recv/file
|
||||
|
||||
# Same, but with a compressed stream.
|
||||
log_must eval "zfs send -c $POOL/fs@snap1 >$BACKDIR/stream.snap1c"
|
||||
assert_split_with_tail "$BACKDIR/stream.snap1c"
|
||||
log_must eval "zfs recv $POOL/recvc <$BACKDIR/stream.snap1c"
|
||||
log_must diff /$POOL/fs/file /$POOL/recvc/file
|
||||
|
||||
# Incremental send without -L: rewrite part of the split block and send the
|
||||
# delta, both plain and compressed, on top of the initial receives.
|
||||
log_must dd if=/dev/urandom of=/$POOL/fs/file bs=1000 count=100 seek=100 conv=notrunc
|
||||
log_must zfs snapshot $POOL/fs@snap2
|
||||
log_must eval "zfs send -i @snap1 $POOL/fs@snap2 >$BACKDIR/stream.snap2"
|
||||
log_must eval "zfs recv -F $POOL/recv <$BACKDIR/stream.snap2"
|
||||
log_must diff /$POOL/fs/file /$POOL/recv/file
|
||||
|
||||
log_must eval "zfs send -c -i @snap1 $POOL/fs@snap2 >$BACKDIR/stream.snap2c"
|
||||
log_must eval "zfs recv -F $POOL/recvc <$BACKDIR/stream.snap2c"
|
||||
log_must diff /$POOL/fs/file /$POOL/recvc/file
|
||||
|
||||
# Follow-up incremental *with* large blocks: rewrite the whole file and send it
|
||||
# as a single large block on top of the split representation already received.
|
||||
# This exercises rewriting an object that was received in split form.
|
||||
log_must dd if=/dev/urandom of=/$POOL/fs/file bs=1000 count=300
|
||||
log_must zfs snapshot $POOL/fs@snap3
|
||||
log_must eval "zfs send -L -i @snap2 $POOL/fs@snap3 >$BACKDIR/stream.snap3L"
|
||||
log_must eval "zfs recv -F $POOL/recv <$BACKDIR/stream.snap3L"
|
||||
log_must diff /$POOL/fs/file /$POOL/recv/file
|
||||
|
||||
# Multi-block object: a file spanning several 1m blocks is split into many
|
||||
# 128K chunks across blocks. Verify the batched split records are received
|
||||
# and reproduce the file exactly.
|
||||
log_must zfs create -o recordsize=1m $POOL/fsmulti
|
||||
log_must dd if=/dev/urandom of=/$POOL/fsmulti/file bs=1024k count=3
|
||||
log_must zfs snapshot $POOL/fsmulti@snap1
|
||||
log_must eval "zfs send $POOL/fsmulti@snap1 >$BACKDIR/stream.multi"
|
||||
typeset nsplit=$(write_sizes "$BACKDIR/stream.multi" | grep -c "^$SPLIT$")
|
||||
log_note "multi-block stream: 128K WRITEs=$nsplit"
|
||||
[[ $nsplit -ge 16 ]] || log_fail "expected many 128K WRITE records for a multi-block object"
|
||||
log_must eval "zfs recv $POOL/recvmulti <$BACKDIR/stream.multi"
|
||||
log_must diff /$POOL/fsmulti/file /$POOL/recvmulti/file
|
||||
|
||||
log_pass "A non-large-block send of a large non-power-of-2 block is received intact"
|
||||
Reference in new issue
Block a user