Skip to content

Commit 0229f82

Browse files
committed
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. Signed-off-by: MorganaFuture <103630661+MorganaFuture@users.noreply.github.com> Closes #17829
1 parent e78fa48 commit 0229f82

4 files changed

Lines changed: 95 additions & 8 deletions

File tree

module/zfs/dmu_recv.c

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2272,14 +2272,20 @@ flush_write_batch_impl(struct receive_writer_arg *rwa)
22722272

22732273
if (drrw->drr_logical_size != dn->dn_datablksz) {
22742274
/*
2275-
* The WRITE record is larger than the object's block
2276-
* size. We must be receiving an incremental
2277-
* large-block stream into a dataset that previously did
2278-
* a non-large-block receive. Lightweight writes must
2279-
* be exactly one block, so we need to decompress the
2280-
* data (if compressed) and do a normal dmu_write().
2275+
* The WRITE record size does not match the object's
2276+
* block size. This can happen when the record is
2277+
* larger than the block size (an incremental large-block
2278+
* stream received into a dataset that previously did a
2279+
* non-large-block receive), and also when it is a
2280+
* smaller trailing chunk produced by splitting a large
2281+
* block for a stream sent without large blocks: a
2282+
* single-block object can have a non-power-of-2 block
2283+
* size, so its final SPA_OLD_MAXBLOCKSIZE-sized chunk
2284+
* may be shorter than the block size. Either way a
2285+
* lightweight write is not possible (those must cover
2286+
* exactly one block), so we decompress the data (if
2287+
* compressed) and do a normal dmu_write().
22812288
*/
2282-
ASSERT3U(drrw->drr_logical_size, >, dn->dn_datablksz);
22832289
if (DRR_WRITE_COMPRESSED(drrw)) {
22842290
abd_t *decomp_abd =
22852291
abd_alloc_linear(drrw->drr_logical_size,

tests/runfiles/common.run

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,8 @@ tests = ['recv_dedup', 'recv_dedup_encrypted_zvol', 'rsend_001_pos',
10091009
'send_encrypted_freeobjects', 'send_encrypted_hierarchy',
10101010
'send_encrypted_props', 'send_encrypted_truncated_files',
10111011
'send_freeobjects', 'send_realloc_files', 'send_realloc_encrypted_files',
1012-
'send_spill_block', 'send_holds', 'send_hole_birth', 'send_mixed_raw',
1012+
'send_spill_block', 'send_split_large_block', 'send_holds',
1013+
'send_hole_birth', 'send_mixed_raw',
10131014
'send-wR_encrypted_zvol',
10141015
'send_partial_dataset', 'send_invalid',
10151016
'send_large_blocks_incremental', 'send_large_blocks_initial',

tests/zfs-tests/tests/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2169,6 +2169,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
21692169
functional/rsend/send_realloc_encrypted_files.ksh \
21702170
functional/rsend/send_realloc_files.ksh \
21712171
functional/rsend/send_spill_block.ksh \
2172+
functional/rsend/send_split_large_block.ksh \
21722173
functional/rsend/send-wR_encrypted_zvol.ksh \
21732174
functional/rsend/setup.ksh \
21742175
functional/scrub_mirror/cleanup.ksh \
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/ksh -p
2+
# SPDX-License-Identifier: CDDL-1.0
3+
4+
#
5+
# This file and its contents are supplied under the terms of the
6+
# Common Development and Distribution License ("CDDL"), version 1.0.
7+
# You may only use this file in accordance with the terms of version
8+
# 1.0 of the CDDL.
9+
#
10+
# A full copy of the text of the CDDL should have accompanied this
11+
# source. A copy of the CDDL is also available via the Internet at
12+
# http://www.illumos.org/license/CDDL.
13+
#
14+
15+
#
16+
# Copyright (c) 2026 by MorganaFuture. All rights reserved.
17+
#
18+
19+
. $STF_SUITE/include/libtest.shlib
20+
. $STF_SUITE/tests/functional/rsend/rsend.kshlib
21+
22+
#
23+
# Description:
24+
# A dataset with a large recordsize can hold a single-block file whose block
25+
# size is not a power of two. Sending it without large blocks (no -L) splits
26+
# that block into SPA_OLD_MAXBLOCKSIZE (128K) chunks, and the final chunk is
27+
# smaller than the block size. The receiver must accept such a stream, for
28+
# both an initial and an incremental send, and reproduce the file exactly.
29+
#
30+
# Previously the receiver assumed a size-mismatched WRITE record was always
31+
# larger than the block size, so this shorter trailing chunk tripped a bad
32+
# assertion and hung the receive_writer thread on debug builds (#17829).
33+
#
34+
35+
verify_runnable "both"
36+
37+
function cleanup
38+
{
39+
rm -f $BACKDIR/fs@*
40+
datasetexists $POOL/fs && destroy_dataset $POOL/fs "-rR"
41+
datasetexists $POOL/recv && destroy_dataset $POOL/recv "-rR"
42+
datasetexists $POOL/recvc && destroy_dataset $POOL/recvc "-rR"
43+
}
44+
45+
log_assert "A non-large-block send of a large non-power-of-2 block is received intact"
46+
log_onexit cleanup
47+
48+
log_must zfs create -o recordsize=1m $POOL/fs
49+
50+
# 300000 bytes in a recordsize=1m dataset is stored in a single block whose
51+
# size is rounded up to 300032 bytes (293K): larger than 128K and not a power
52+
# of two, so a send without -L must emit a short trailing 128K-split chunk.
53+
log_must dd if=/dev/urandom of=/$POOL/fs/file bs=1000 count=300
54+
log_must zfs snapshot $POOL/fs@snap1
55+
56+
# Guard the premise: were the block ever a power of two the split would be
57+
# even and would no longer exercise the short-tail path this test targets.
58+
typeset dblk=$(zdb -dddd $POOL/fs 2>/dev/null | awk '/ZFS plain file/ {print $4}')
59+
log_must test "$dblk" = "293K"
60+
61+
# Initial send without -L, both plain and compressed, must be received and
62+
# reproduce the file byte-for-byte.
63+
log_must eval "zfs send $POOL/fs@snap1 >$BACKDIR/fs@snap1"
64+
log_must eval "zfs recv $POOL/recv <$BACKDIR/fs@snap1"
65+
log_must diff /$POOL/fs/file /$POOL/recv/file
66+
67+
log_must eval "zfs send -c $POOL/fs@snap1 >$BACKDIR/fs@snap1c"
68+
log_must eval "zfs recv $POOL/recvc <$BACKDIR/fs@snap1c"
69+
log_must diff /$POOL/fs/file /$POOL/recvc/file
70+
71+
# Incremental send without -L: rewrite part of the split block and send the
72+
# delta on top of the initial receive.
73+
log_must dd if=/dev/urandom of=/$POOL/fs/file bs=1000 count=100 seek=100 conv=notrunc
74+
log_must zfs snapshot $POOL/fs@snap2
75+
log_must eval "zfs send -i @snap1 $POOL/fs@snap2 >$BACKDIR/fs@snap2"
76+
log_must eval "zfs recv -F $POOL/recv <$BACKDIR/fs@snap2"
77+
log_must diff /$POOL/fs/file /$POOL/recv/file
78+
79+
log_pass "A non-large-block send of a large non-power-of-2 block is received intact"

0 commit comments

Comments
 (0)