* [PATCH] generic/790: test post-EOF gap zeroing persistence
@ 2026-04-22 1:52 Zhang Yi
2026-04-22 13:22 ` Brian Foster
0 siblings, 1 reply; 2+ messages in thread
From: Zhang Yi @ 2026-04-22 1:52 UTC (permalink / raw)
To: fstests, zlang
Cc: linux-ext4, linux-fsdevel, jack, yi.zhang, yi.zhang, yizhang089,
yangerkun
From: Zhang Yi <yi.zhang@huawei.com>
Test that extending a file past a non-block-aligned EOF correctly
zero-fills the gap [old_EOF, block_boundary), and that this zeroing
persists through a filesystem shutdown+remount cycle.
Stale data beyond EOF can persist on disk when append write data blocks
are flushed before the i_size metadata update, or when concurrent append
writeback and mmap writes persist non-zero data past EOF. Subsequent
post-EOF operations (append write, fallocate, truncate up) must
zero-fill and persist the gap to prevent exposing stale data.
The test pollutes the file's last physical block (via FIEMAP + raw
device write) with a sentinel pattern beyond i_size, then performs each
extend operation and verifies the gap is zeroed both in memory and on
disk.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
This is the case Jan Kara pointed out during my work on the ext4
buffered I/O to iomap conversion. This case is similar to generic/363,
but generic/363 doesn't provide persistent testing. For details:
https://lore.kernel.org/linux-ext4/jgotl7vzzuzm6dvz5zfgk6haodxvunb4hq556pzh4hqqwvnhxq@lr3jiedhqh7c/
tests/generic/790 | 155 ++++++++++++++++++++++++++++++++++++++++++
tests/generic/790.out | 4 ++
2 files changed, 159 insertions(+)
create mode 100755 tests/generic/790
create mode 100644 tests/generic/790.out
diff --git a/tests/generic/790 b/tests/generic/790
new file mode 100755
index 00000000..5d8f61f9
--- /dev/null
+++ b/tests/generic/790
@@ -0,0 +1,155 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Huawei. All Rights Reserved.
+#
+# FS QA Test No. 790
+#
+# Test that extending a file past a non-block-aligned EOF correctly zero-fills
+# the gap [old_EOF, block_boundary), and that this zeroing persists through a
+# filesystem shutdown+remount cycle.
+#
+# Stale data beyond EOF can persist on disk when:
+# 1) append write data blocks are flushed before the i_size metadata update,
+# and the system crashes in this window.
+# 2) concurrent append writeback and mmap writes persist non-zero data past EOF.
+#
+# Subsequent post-EOF operations (append write, fallocate, truncate up) must
+# zero-fill and persist the gap to prevent exposing stale data.
+#
+# The test pollutes the file's last physical block (via FIEMAP + raw device
+# write) with a sentinel pattern beyond i_size, then performs each extend
+# operation and verifies the gap is zeroed both in memory and on disk.
+#
+. ./common/preamble
+_begin_fstest auto quick rw shutdown
+
+. ./common/filter
+
+_require_scratch
+_require_block_device $SCRATCH_DEV
+_require_scratch_shutdown
+_require_metadata_journaling $SCRATCH_DEV
+_require_xfs_io_command "fiemap"
+_require_xfs_io_command "falloc"
+_require_xfs_io_command "pwrite"
+_require_xfs_io_command "truncate"
+_require_xfs_io_command "sync_range"
+
+# Check that gap region [offset, offset+nbytes) is entirely zero
+_check_gap_zero()
+{
+ local file="$1"
+ local offset="$2"
+ local nbytes="$3"
+ local label="$4"
+ local data
+ local stripped
+
+ data=$(od -A n -t x1 -j $offset -N $nbytes "$file" 2>/dev/null)
+
+ # Remove whitespace and check if any byte is non-zero
+ stripped=$(echo "$data" | tr -d ' \n\t')
+ if [ -n "$stripped" ] && ! echo "$stripped" | grep -qE "^0+$"; then
+ echo "FAIL: non-zero data in gap [$offset,$((offset + nbytes))) $label"
+ _hexdump -N $((offset + nbytes)) "$file"
+ return 1
+ fi
+ return 0
+}
+
+# Get the physical block offset (in bytes) of the file's first block on device
+_get_phys_offset()
+{
+ local file="$1"
+ local fiemap_output
+ local phys_blk
+
+ fiemap_output=$($XFS_IO_PROG -r -c "fiemap -v" "$file" 2>/dev/null)
+ phys_blk=$(echo "$fiemap_output" | _filter_xfs_io_fiemap | head -1 | awk '{print $3}')
+ if [ -z "$phys_blk" ]; then
+ echo ""
+ return
+ fi
+ # Convert 512-byte blocks to bytes
+ echo $((phys_blk * 512))
+}
+
+_test_eof_zeroing()
+{
+ local test_name="$1"
+ local extend_cmd="$2"
+ local file=$SCRATCH_MNT/testfile_${test_name}
+
+ echo "$test_name" | tee -a $seqres.full
+
+ # Compute non-block-aligned EOF offset
+ local gap_bytes=16
+ local eof_offset=$((blksz - gap_bytes))
+
+ # Step 1: Write one full block to ensure the filesystem allocates a
+ # physical block for the file instead of using inline data.
+ $XFS_IO_PROG -f -c "pwrite -S 0x5a 0 $blksz" -c fsync \
+ "$file" >> $seqres.full 2>&1
+
+ # Step 2: Get physical block offset on device via FIEMAP
+ local phys_offset
+ phys_offset=$(_get_phys_offset "$file")
+ if [ -z "$phys_offset" ]; then
+ _fail "$test_name: failed to get physical block offset via fiemap"
+ fi
+
+ # Step 3: Truncate file to non-block-aligned size and fsync.
+ # The on-disk region [eof_offset, blksz) may or may not be
+ # zeroed by the filesystem at this point.
+ $XFS_IO_PROG -c "truncate $eof_offset" -c fsync \
+ "$file" >> $seqres.full 2>&1
+
+ # Step 4: Unmount and restore the physical block to all-0x5a on disk.
+ # This bypasses the kernel's pagecache EOF-zeroing to ensure
+ # the stale pattern is present on disk. Then remount.
+ _scratch_unmount
+ $XFS_IO_PROG -d -c "pwrite -S 0x5a $phys_offset $blksz" \
+ $SCRATCH_DEV >> $seqres.full 2>&1
+ _scratch_mount >> $seqres.full 2>&1
+
+ # Verify file size is still eof_offset after remount
+ local sz
+ sz=$(stat -c %s "$file")
+ if [ "$sz" -ne "$eof_offset" ]; then
+ _fail "$test_name: file size wrong after remount: $sz != $eof_offset"
+ fi
+
+ # Step 5: Execute the extend operation.
+ $XFS_IO_PROG -c "$extend_cmd" "$file" >> $seqres.full 2>&1
+
+ # Step 6: Verify gap [eof_offset, blksz) is zeroed BEFORE shutdown
+ _check_gap_zero "$file" $eof_offset $gap_bytes "before shutdown" || return 1
+
+ # Step 7: Sync the extended range and shutdown the filesystem with
+ # journal flush. This persists the file size extending, and
+ # the filesystem should persist the zeroed data in the gap
+ # range as well.
+ if [ "$extend_cmd" != "${extend_cmd#pwrite}" ]; then
+ $XFS_IO_PROG -c "sync_range -w $blksz $blksz" \
+ "$file" >> $seqres.full 2>&1
+ fi
+ _scratch_shutdown -f
+
+ # Step 8: Remount and verify gap is still zeroed
+ _scratch_cycle_mount
+ _check_gap_zero "$file" $eof_offset $gap_bytes "after shutdown+remount" || return 1
+}
+
+_scratch_mkfs >> $seqres.full 2>&1
+_scratch_mount
+
+blksz=$(_get_block_size $SCRATCH_MNT)
+
+# Test three variants of EOF-extending operations
+_test_eof_zeroing "append_write" "pwrite -S 0x42 $blksz $blksz"
+_test_eof_zeroing "truncate_up" "truncate $((blksz * 2))"
+_test_eof_zeroing "fallocate" "falloc $blksz $blksz"
+
+# success, all done
+status=0
+exit
diff --git a/tests/generic/790.out b/tests/generic/790.out
new file mode 100644
index 00000000..e5e2cc09
--- /dev/null
+++ b/tests/generic/790.out
@@ -0,0 +1,4 @@
+QA output created by 790
+append_write
+truncate_up
+fallocate
--
2.52.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH] generic/790: test post-EOF gap zeroing persistence
2026-04-22 1:52 [PATCH] generic/790: test post-EOF gap zeroing persistence Zhang Yi
@ 2026-04-22 13:22 ` Brian Foster
0 siblings, 0 replies; 2+ messages in thread
From: Brian Foster @ 2026-04-22 13:22 UTC (permalink / raw)
To: Zhang Yi
Cc: fstests, zlang, linux-ext4, linux-fsdevel, jack, yi.zhang,
yizhang089, yangerkun
On Wed, Apr 22, 2026 at 09:52:46AM +0800, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> Test that extending a file past a non-block-aligned EOF correctly
> zero-fills the gap [old_EOF, block_boundary), and that this zeroing
> persists through a filesystem shutdown+remount cycle.
>
> Stale data beyond EOF can persist on disk when append write data blocks
> are flushed before the i_size metadata update, or when concurrent append
> writeback and mmap writes persist non-zero data past EOF. Subsequent
> post-EOF operations (append write, fallocate, truncate up) must
> zero-fill and persist the gap to prevent exposing stale data.
>
> The test pollutes the file's last physical block (via FIEMAP + raw
> device write) with a sentinel pattern beyond i_size, then performs each
> extend operation and verifies the gap is zeroed both in memory and on
> disk.
>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> This is the case Jan Kara pointed out during my work on the ext4
> buffered I/O to iomap conversion. This case is similar to generic/363,
> but generic/363 doesn't provide persistent testing. For details:
>
> https://lore.kernel.org/linux-ext4/jgotl7vzzuzm6dvz5zfgk6haodxvunb4hq556pzh4hqqwvnhxq@lr3jiedhqh7c/
>
> tests/generic/790 | 155 ++++++++++++++++++++++++++++++++++++++++++
> tests/generic/790.out | 4 ++
> 2 files changed, 159 insertions(+)
> create mode 100755 tests/generic/790
> create mode 100644 tests/generic/790.out
>
> diff --git a/tests/generic/790 b/tests/generic/790
> new file mode 100755
> index 00000000..5d8f61f9
> --- /dev/null
> +++ b/tests/generic/790
> @@ -0,0 +1,155 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2026 Huawei. All Rights Reserved.
> +#
> +# FS QA Test No. 790
> +#
> +# Test that extending a file past a non-block-aligned EOF correctly zero-fills
> +# the gap [old_EOF, block_boundary), and that this zeroing persists through a
> +# filesystem shutdown+remount cycle.
> +#
Nice test! This is a great idea.
> +# Stale data beyond EOF can persist on disk when:
> +# 1) append write data blocks are flushed before the i_size metadata update,
> +# and the system crashes in this window.
Maybe it's wording or I'm missing something, but how would "append write
data blocks" be flushed before i_size updates? Wouldn't writeback toss
them or zero the post-eof range of a folio? Do you mean to refer to
"on-disk size update" specifically (where I'm reading it as
inode->i_isize)?
> +# 2) concurrent append writeback and mmap writes persist non-zero data past EOF.
> +#
> +# Subsequent post-EOF operations (append write, fallocate, truncate up) must
> +# zero-fill and persist the gap to prevent exposing stale data.
> +#
> +# The test pollutes the file's last physical block (via FIEMAP + raw device
> +# write) with a sentinel pattern beyond i_size, then performs each extend
> +# operation and verifies the gap is zeroed both in memory and on disk.
> +#
...
> +_test_eof_zeroing()
> +{
> + local test_name="$1"
> + local extend_cmd="$2"
> + local file=$SCRATCH_MNT/testfile_${test_name}
> +
> + echo "$test_name" | tee -a $seqres.full
> +
> + # Compute non-block-aligned EOF offset
> + local gap_bytes=16
> + local eof_offset=$((blksz - gap_bytes))
> +
> + # Step 1: Write one full block to ensure the filesystem allocates a
> + # physical block for the file instead of using inline data.
> + $XFS_IO_PROG -f -c "pwrite -S 0x5a 0 $blksz" -c fsync \
> + "$file" >> $seqres.full 2>&1
> +
> + # Step 2: Get physical block offset on device via FIEMAP
> + local phys_offset
> + phys_offset=$(_get_phys_offset "$file")
> + if [ -z "$phys_offset" ]; then
> + _fail "$test_name: failed to get physical block offset via fiemap"
> + fi
> +
> + # Step 3: Truncate file to non-block-aligned size and fsync.
> + # The on-disk region [eof_offset, blksz) may or may not be
> + # zeroed by the filesystem at this point.
> + $XFS_IO_PROG -c "truncate $eof_offset" -c fsync \
> + "$file" >> $seqres.full 2>&1
> +
> + # Step 4: Unmount and restore the physical block to all-0x5a on disk.
> + # This bypasses the kernel's pagecache EOF-zeroing to ensure
> + # the stale pattern is present on disk. Then remount.
> + _scratch_unmount
> + $XFS_IO_PROG -d -c "pwrite -S 0x5a $phys_offset $blksz" \
> + $SCRATCH_DEV >> $seqres.full 2>&1
> + _scratch_mount >> $seqres.full 2>&1
> +
> + # Verify file size is still eof_offset after remount
> + local sz
> + sz=$(stat -c %s "$file")
> + if [ "$sz" -ne "$eof_offset" ]; then
> + _fail "$test_name: file size wrong after remount: $sz != $eof_offset"
> + fi
I was initially curious why we'd want to do this, but after further
thought I wonder if it might make more sense to check file size against
the extended size after the shutdown/mount cycle below (but before
checking the gap range). That way we know the size update was
logged/recovered correctly and we're about to read from a file range
within eof. Hm?
Those couple nits aside this all looks pretty good to me.
Brian
> +
> + # Step 5: Execute the extend operation.
> + $XFS_IO_PROG -c "$extend_cmd" "$file" >> $seqres.full 2>&1
> +
> + # Step 6: Verify gap [eof_offset, blksz) is zeroed BEFORE shutdown
> + _check_gap_zero "$file" $eof_offset $gap_bytes "before shutdown" || return 1
> +
> + # Step 7: Sync the extended range and shutdown the filesystem with
> + # journal flush. This persists the file size extending, and
> + # the filesystem should persist the zeroed data in the gap
> + # range as well.
> + if [ "$extend_cmd" != "${extend_cmd#pwrite}" ]; then
> + $XFS_IO_PROG -c "sync_range -w $blksz $blksz" \
> + "$file" >> $seqres.full 2>&1
> + fi
> + _scratch_shutdown -f
> +
> + # Step 8: Remount and verify gap is still zeroed
> + _scratch_cycle_mount
> + _check_gap_zero "$file" $eof_offset $gap_bytes "after shutdown+remount" || return 1
> +}
> +
> +_scratch_mkfs >> $seqres.full 2>&1
> +_scratch_mount
> +
> +blksz=$(_get_block_size $SCRATCH_MNT)
> +
> +# Test three variants of EOF-extending operations
> +_test_eof_zeroing "append_write" "pwrite -S 0x42 $blksz $blksz"
> +_test_eof_zeroing "truncate_up" "truncate $((blksz * 2))"
> +_test_eof_zeroing "fallocate" "falloc $blksz $blksz"
> +
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/generic/790.out b/tests/generic/790.out
> new file mode 100644
> index 00000000..e5e2cc09
> --- /dev/null
> +++ b/tests/generic/790.out
> @@ -0,0 +1,4 @@
> +QA output created by 790
> +append_write
> +truncate_up
> +fallocate
> --
> 2.52.0
>
>
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-04-22 13:22 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-22 1:52 [PATCH] generic/790: test post-EOF gap zeroing persistence Zhang Yi
2026-04-22 13:22 ` Brian Foster
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox