diff --git a/daxfs/file.c b/daxfs/file.c index 9c4a6b3..3213037 100644 --- a/daxfs/file.c +++ b/daxfs/file.c @@ -471,6 +471,37 @@ static ssize_t daxfs_write_iter(struct kiocb *iocb, struct iov_iter *from) return total; } +/* + * Discard overlay data from @from up to the old end of file @to. + * + * The pages are zeroed rather than unmapped and freed. Freeing them would + * mean deleting keys from the open-addressed overlay hash, which needs + * tombstones it has no state bit for, and for an inode with base image data + * behind it dropping the overlay page would re-expose the base contents + * instead of the zeros POSIX requires. Zeroing is correct for both cases and + * leaves the pages ready to be reused if the file is extended again. + */ +static void daxfs_truncate_overlay(struct daxfs_info *info, + struct inode *inode, loff_t from, loff_t to) +{ + u64 pgoff = from >> PAGE_SHIFT; + u32 intra = from & (PAGE_SIZE - 1); + void *page; + + if (intra) { + page = daxfs_overlay_get_page_cached(info, inode, pgoff); + if (page) + memset(page + intra, 0, PAGE_SIZE - intra); + pgoff++; + } + + for (; ((loff_t)pgoff << PAGE_SHIFT) < to; pgoff++) { + page = daxfs_overlay_get_page_cached(info, inode, pgoff); + if (page) + memset(page, 0, PAGE_SIZE); + } +} + static int daxfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, struct iattr *attr) { @@ -478,6 +509,7 @@ static int daxfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, struct daxfs_info *info = DAXFS_SB(inode->i_sb); struct daxfs_ovl_inode_entry ie; struct daxfs_ovl_inode_entry *existing; + loff_t old_size; int ret; if (!info->overlay) @@ -487,6 +519,13 @@ static int daxfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (ret) return ret; + /* + * Capture the old size before anything overwrites it. Another host may + * have extended the file past our cached i_size, and those pages need + * discarding too. + */ + old_size = i_size_read(inode); + /* * If overlay inode exists, update individual fields in-place. * Each WRITE_ONCE is atomic for its field width, so concurrent @@ -494,6 +533,11 @@ static int daxfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, */ existing = daxfs_overlay_get_inode(info, inode->i_ino); if (existing) { + loff_t ovl_size = le64_to_cpu(READ_ONCE(existing->size)); + + if (ovl_size > old_size) + old_size = ovl_size; + if (attr->ia_valid & ATTR_SIZE) WRITE_ONCE(existing->size, cpu_to_le64(attr->ia_size)); if (attr->ia_valid & ATTR_MODE) @@ -534,6 +578,11 @@ static int daxfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry, if (attr->ia_valid & ATTR_SIZE) { truncate_setsize(inode, attr->ia_size); daxfs_update_blocks(inode); + + /* After i_size shrinks, so readers cannot see the stale tail */ + if (attr->ia_size < old_size) + daxfs_truncate_overlay(info, inode, attr->ia_size, + old_size); } setattr_copy(idmap, inode, attr); diff --git a/tests/test_overlay.sh b/tests/test_overlay.sh index c9c5244..8da30b4 100755 --- a/tests/test_overlay.sh +++ b/tests/test_overlay.sh @@ -556,6 +556,44 @@ test_overlay_truncate() { pass "Truncate via overlay" } +test_overlay_truncate_zeroes() { + run_test "Truncate discards data (no stale bytes on re-extend)" + + local f="$MNT/trunc-stale.bin" + + printf 'AAAAAAAAAAAAAAAA' > "$f" \ + || { fail "Truncate zeroes" "Failed to create file"; return; } + truncate -s 0 "$f" \ + || { fail "Truncate zeroes" "Failed to truncate"; return; } + + # Re-extend past the old data. Everything below the write must read zero. + dd if=/dev/zero of="$f" bs=1 count=1 seek=65536 conv=notrunc status=none \ + || { fail "Truncate zeroes" "Failed to re-extend"; return; } + + local head + head=$(dd if="$f" bs=16 count=1 status=none | tr -d '\0') + if [ -n "$head" ]; then + fail "Truncate zeroes" "stale data after truncate: '$head'" + return + fi + + # Same again for a partial page: the tail of the surviving page must go. + printf 'BBBBBBBBBBBBBBBB' > "$f" + truncate -s 4 "$f" + dd if=/dev/zero of="$f" bs=1 count=1 seek=4096 conv=notrunc status=none \ + || { fail "Truncate zeroes" "Failed to re-extend (partial)"; return; } + + local tail + tail=$(dd if="$f" bs=1 skip=4 count=12 status=none | tr -d '\0') + if [ -n "$tail" ]; then + fail "Truncate zeroes" "stale tail in partial page: '$tail'" + return + fi + + rm -f "$f" + pass "Truncate discards data (no stale bytes on re-extend)" +} + # # Empty mode tests # @@ -815,6 +853,7 @@ main() { test_overlay_symlink test_overlay_rename test_overlay_truncate + test_overlay_truncate_zeroes # Empty mode tests setup_empty