Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions daxfs/file.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,45 @@ 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)
{
struct inode *inode = d_inode(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)
Expand All @@ -487,13 +519,25 @@ 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
* setattr calls touching different fields won't clobber each other.
*/
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)
Expand Down Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions tests/test_overlay.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -815,6 +853,7 @@ main() {
test_overlay_symlink
test_overlay_rename
test_overlay_truncate
test_overlay_truncate_zeroes

# Empty mode tests
setup_empty
Expand Down