Re: [PATCH v2] generic/795: add unaligned boundary test cases for WRITE_ZEROES
Zhang Yi <[email protected]> Fri, 10 Jul 2026 16:23:27 +0800
| Newsgroups | org.kernel.vger.fstests,org.kernel.vger.linux-ext4,org.kernel.vger.linux-xfs |
|---|---|
| Message-ID | <[email protected]> |
+CC ext4 list On 7/9/2026 4:30 PM, Pankaj Raghav wrote: > During the review of WRITE_ZEROES support to XFS, Zhang Yi pointed out > some important semantics for the boundary blocks when WRITE_ZEROES > command is used.[1] > > The test cases check the boundary blocks when they are in different > states and WRITE_ZEROES are issued that straddle the boundary. Along with that, > test cases have been added when a EoF straddles an allocation unit. > > [1] https://lore.kernel.org/linux-xfs/[email protected]/ > > Signed-off-by: Pankaj Raghav <[email protected]> Thank you for the test! The changes look good to me, and it works well with my ext4 fixes as well. Reviewed-by: Zhang Yi <[email protected]> Unfortunately, I've found a bug in filefrag that causes delalloc extents to be reported with a length of 0. This means this test case can miss delalloc extents. The issue is in misc/filefrag.c's print_extent_info(), where ext_len is incorrectly set to 0 for the FIEMAP_EXTENT_UNKNOWN or EXT4_FIEMAP_EXTENT_HOLE cases. Could you please send a patch to fix this as well? Thanks, Yi. > --- > CHanges since v1: > - Use filefrag instead of FIEMAP as the latter uses FIEMAP_FLAG_SYNC by > default when used via xfs_io (Zhang yi) > - Add an extra testcase for dirty-unwritten edges (Zhang Yi) > > common/rc | 2 +- > tests/generic/795 | 187 ++++++++++++++++++++++++++++++++++++++++++ > tests/generic/795.out | 19 +++++ > 3 files changed, 207 insertions(+), 1 deletion(-) > create mode 100755 tests/generic/795 > create mode 100644 tests/generic/795.out > > diff --git a/common/rc b/common/rc > index 79189e7e..5e59968f 100644 > --- a/common/rc > +++ b/common/rc > @@ -3008,7 +3008,7 @@ _require_xfs_io_command() > testio=`$XFS_IO_PROG -F -f -c "$command $param 0 1m" $testfile 2>&1` > param_checked="$param" > ;; > - "fpunch" | "fcollapse" | "zero" | "fzero" | "finsert" | "funshare") > + "fpunch" | "fcollapse" | "zero" | "fzero" | "finsert" | "funshare" | "fwzero") > local blocksize=$(_get_file_block_size $TEST_DIR) > testio=`$XFS_IO_PROG -F -f -c "pwrite 0 $((5 * $blocksize))" \ > -c "fsync" -c "$command $blocksize $((2 * $blocksize))" \ > diff --git a/tests/generic/795 b/tests/generic/795 > new file mode 100755 > index 00000000..b1e05666 > --- /dev/null > +++ b/tests/generic/795 > @@ -0,0 +1,187 @@ > +#! /bin/bash > +# SPDX-License-Identifier: GPL-2.0 > +# Copyright (c) 2026 Samsung Electronics. All Rights Reserved. > +# > +# FS QA Test 795 > +# > +# Verify FALLOC_FL_WRITE_ZEROES (xfs_io "fwzero") on an unaligned range, > +# exercising every state the two boundary allocation units can be in. > +# > +# WRITE_ZEROES must leave the whole requested range backed by *written* > +# (zeroed) extents while preserving the out-of-range bytes of the partial > +# boundary units. The scenarios are: written_edges, hole_edges, > +# clean_unwritten_edges, delalloc_edges and dirty_unwritten_edges. > +# > +# It then covers the EOF cases: a WRITE_ZEROES that *extends* the file to a > +# non-unit-aligned size, followed by a further extension. WRITE_ZEROES must > +# never leave written blocks beyond EOF. > + > +. ./common/preamble > +_begin_fstest auto quick prealloc fiemap > + > +testfile=$TEST_DIR/$seq.testfile > + > +_cleanup() > +{ > + cd / > + rm -r -f $tmp.* > + rm -f $testfile > +} > + > +. ./common/filter > + > +_require_test > +_require_xfs_io_command "fwzero" > +_require_xfs_io_command "falloc" > +_require_xfs_io_command "truncate" > +_require_command "$FILEFRAG_PROG" filefrag > + > +# The allocation unit: rt extent size on an rt config, otherwise the block > +# size. Deriving the range from this makes the head/tail units straddle rt > +# extents when rextsize > 1. > +u=$(_get_file_block_size $TEST_DIR) > + > +# The fs block size. On rt with rextsize > 1 the allocation unit u is the rt > +# extent (u > b). A WRITE_ZEROES that extends EOF must only write out to the > +# block-rounded EOF and leave the rest of the straddling rt extent unwritten, > +# rather than written past EOF. > +b=$(_get_block_size $TEST_DIR) > + > +# Unaligned range within EOF: start 1.5 units in, span 4 units, so both the > +# head unit [u, 2u) and the tail unit [5u, 6u) are only partially covered, > +# with fully covered units in between and data on both sides. > +off=$((u + u / 2)) > +len=$((4 * u)) > +filesz=$((8 * u)) > +pattern=0xab > + > +# Expected images. For the written/delalloc/dirty-unwritten cases the pattern > +# surrounds the zeroed range; for the hole/clean-unwritten cases the file reads > +# back all zeroes. > +$XFS_IO_PROG -f -c "pwrite -S $pattern 0 $filesz" $tmp.pattern >> $seqres.full 2>&1 > +$XFS_IO_PROG -c "pwrite -S 0 $off $len" $tmp.pattern >> $seqres.full 2>&1 > +$XFS_IO_PROG -f -c "truncate $filesz" $tmp.zero >> $seqres.full 2>&1 > + > +# _filter_filefrag emits "phys#len#logical#flags" in bytes. filefrag has no > +# range option, so clip the extents to the byte window [s, e) with awk before > +# grepping - an extent [$3, $3+$2) overlaps the window iff $3 < e && $3+$2 > s. > +_filefrag_window() > +{ > + local file=$1 start=$2 end=$3 > + > + $FILEFRAG_PROG -v "$file" | _filter_filefrag | \ > + $AWK_PROG -F'#' -v s=$start -v e=$end '$3 < e && $3 + $2 > s' > +} > + > +# Assert the whole requested range is backed by written extents immediately > +# after fwzero (before any sync/remount): no unwritten, no delalloc. > +# The extent state is queried with filefrag: xfs_io's fiemap always sets > +# FIEMAP_FLAG_SYNC, which would flush and convert delalloc to written before > +# reporting. > +_check_range_written() > +{ > + local file=$1 > + local bad > + > + $FILEFRAG_PROG -v "$file" >> $seqres.full 2>&1 > + bad=$(_filefrag_window "$file" $off $((off + len)) | \ > + grep -E -c 'unwritten|delalloc|unknown') > + if [ "$bad" -eq 0 ]; then > + echo "extents: range fully written" > + else > + echo "extents: FAIL - $bad unwritten/delalloc extent(s) in range" > + fi > +} > + > +_check_data() > +{ > + local file=$1 > + local want=$2 > + > + if cmp -s "$file" "$want"; then > + echo "data: matches expected image" > + else > + echo "data: FAIL - mismatch against expected image" > + cmp "$file" "$want" | head > + fi > +} > + > +# Assert the tail past the block-rounded EOF holds no *written* extents: it must > +# be unwritten or a hole. > +_check_eof_tail() > +{ > + local file=$1 start=$2 len=$3 > + local bad > + > + if [ "$len" -le 0 ]; then > + echo "extents: eof tail unwritten" > + return > + fi > + $FILEFRAG_PROG -v "$file" >> $seqres.full 2>&1 > + bad=$(_filefrag_window "$file" $start $((start + len)) | \ > + grep -E -v -c 'unwritten|delalloc|unknown') > + if [ "$bad" -eq 0 ]; then > + echo "extents: eof tail unwritten" > + else > + echo "extents: FAIL - $bad written extent(s) past block-rounded EOF" > + fi > +} > + > +_run_case() > +{ > + local name=$1 > + local want=$2 > + > + echo "=== $name ===" > + > + $XFS_IO_PROG -c "fwzero $off $len" $testfile >> $seqres.full 2>&1 > + > + echo "== $name filefrag after fwzero ==" >> $seqres.full > + _check_range_written $testfile > + _test_cycle_mount > + _check_data $testfile $want > + > + rm -f $testfile > +} > + > +# 1) Boundary units already written, within i_size. > +$XFS_IO_PROG -f -c "pwrite -S $pattern 0 $filesz" -c fsync $testfile \ > + >> $seqres.full 2>&1 > +_run_case "written_edges" $tmp.pattern > + > +# 2) Boundary units are holes (sparse file). > +$XFS_IO_PROG -f -c "truncate $filesz" $testfile >> $seqres.full 2>&1 > +_run_case "hole_edges" $tmp.zero > + > +# 3) Boundary units are clean unwritten preallocation: read back as zeroes. > +$XFS_IO_PROG -f -c "falloc 0 $filesz" $testfile >> $seqres.full 2>&1 > +_run_case "clean_unwritten_edges" $tmp.zero > + > +# 4) Boundary units are dirty/delalloc. > +$XFS_IO_PROG -f -c "pwrite -S $pattern 0 $filesz" $testfile >> $seqres.full 2>&1 > +_run_case "delalloc_edges" $tmp.pattern > + > +# 5) Boundary units are dirty unwritten. > +$XFS_IO_PROG -f -c "falloc 0 $filesz" -c "pwrite -S $pattern 0 $filesz" \ > + $testfile >> $seqres.full 2>&1 > +_run_case "dirty_unwritten_edges" $tmp.pattern > + > +# 6) EOF cases. A WRITE_ZEROES that extends the file to a non-unit-aligned > +# size, followed by a further extend. > +eof=$((3 * u + 1)) > + > +echo "=== eof_then_truncate ===" > +$XFS_IO_PROG -f -c "fwzero 0 $eof" $testfile >> $seqres.full 2>&1 > +# The write must stop at the block-rounded EOF; the rest of the rt extent that > +# straddles EOF must be unwritten, not written past EOF. > +eof_ru=$(( (eof + b - 1) & ~(b - 1) )) > +_check_eof_tail $testfile $eof_ru $((4 * u - eof_ru)) > + > +$XFS_IO_PROG -c "truncate $filesz" $testfile >> $seqres.full 2>&1 > +_test_cycle_mount > +_check_data $testfile $tmp.zero > +rm -f $testfile > + > +# success, all done > +status=0 > +exit > diff --git a/tests/generic/795.out b/tests/generic/795.out > new file mode 100644 > index 00000000..dca6e571 > --- /dev/null > +++ b/tests/generic/795.out > @@ -0,0 +1,19 @@ > +QA output created by 795 > +=== written_edges === > +extents: range fully written > +data: matches expected image > +=== hole_edges === > +extents: range fully written > +data: matches expected image > +=== clean_unwritten_edges === > +extents: range fully written > +data: matches expected image > +=== delalloc_edges === > +extents: range fully written > +data: matches expected image > +=== dirty_unwritten_edges === > +extents: range fully written > +data: matches expected image > +=== eof_then_truncate === > +extents: eof tail unwritten > +data: matches expected image > > base-commit: ffc8bad17e5b2f56e48dbac43f7c5ae8ac368fe5