linux-kselftest.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/2] fuse: zero the partial EOF page when extending a file
@ 2026-07-31 20:38 Jimmy Zuber
  2026-07-31 20:38 ` [PATCH 1/2] " Jimmy Zuber
  2026-07-31 20:38 ` [PATCH 2/2] selftests/fuse: test post-EOF page zeroing when a file is extended Jimmy Zuber
  0 siblings, 2 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-07-31 20:38 UTC (permalink / raw)
  To: Miklos Szeredi, Shuah Khan; +Cc: fuse-devel, linux-kernel, linux-kselftest

Extending a fuse file past a non-page-aligned EOF does not zero the tail of
the old last page.  When that page is cached and has been mmap-dirtied
beyond the old EOF, the now in-bounds tail is served to later reads as stale
data rather than zeros, which violates POSIX file-extension semantics.

This is the same class of bug that commit b1817b18ff20 ("NFS: Protect
against 'eof page pollution'") fixed for the NFS client; fuse has the
equivalent gap on its non-writeback_cache, FOPEN_KEEP_CACHE path.
pagecache_isize_extended() cannot be reused because it is a no-op when
i_blocksize() >= PAGE_SIZE, which always holds for a non-fuseblk mount, so
patch 1 open-codes the zeroing and calls it from all three paths that extend
a file: buffered write, setattr/truncate, and fallocate.  Patch 2 adds a
self-contained raw /dev/fuse selftest covering each path; every case fails
without patch 1 and passes with it.

Tested by booting the patched kernel under User-Mode Linux and running the
new selftest (4/4 pass; 4/4 fail without patch 1), and reproduced against
libfuse's passthrough_ll example daemon (-o cache=always,no_writeback).

Jimmy Zuber (2):
  fuse: zero the partial EOF page when extending a file
  selftests/fuse: test post-EOF page zeroing when a file is extended

 fs/fuse/dir.c                                 |   3 +
 fs/fuse/file.c                                |  56 +++
 fs/fuse/fuse_i.h                              |   1 +
 .../selftests/filesystems/fuse/.gitignore     |   1 +
 .../selftests/filesystems/fuse/Makefile       |   3 +
 .../filesystems/fuse/write_extend_eof_test.c  | 368 ++++++++++++++++++
 6 files changed, 432 insertions(+)
 create mode 100644 tools/testing/selftests/filesystems/fuse/write_extend_eof_test.c


base-commit: 7d87a5a284bb34edb3f4e7e312ef403b3385a7b7
-- 
2.50.1


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH 1/2] fuse: zero the partial EOF page when extending a file
  2026-07-31 20:38 [PATCH 0/2] fuse: zero the partial EOF page when extending a file Jimmy Zuber
@ 2026-07-31 20:38 ` Jimmy Zuber
  2026-07-31 20:38 ` [PATCH 2/2] selftests/fuse: test post-EOF page zeroing when a file is extended Jimmy Zuber
  1 sibling, 0 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-07-31 20:38 UTC (permalink / raw)
  To: Miklos Szeredi, Shuah Khan; +Cc: fuse-devel, linux-kernel, linux-kselftest

Extending a fuse file past a non-page-aligned EOF does not zero the tail of
the old last page.  If that page is cached and was dirtied beyond the old
EOF -- e.g. an application mmap()ed the EOF page and stored into the region
past EOF, which is undefined until the file grows -- the now in-bounds tail
is exposed to subsequent reads as stale data instead of zeros, in violation
of POSIX file-extension semantics.

Other filesystems zero this via pagecache_isize_extended(), but that helper
is a no-op for fuse: it returns early when i_blocksize() >= PAGE_SIZE, and
a non-fuseblk fuse mount has s_blocksize == PAGE_SIZE (the server-supplied
st_blksize only sets fi->cached_i_blkbits, not i_blkbits).  The NFS client
hit the same problem and open-codes the zeroing in
nfs_truncate_last_folio(); add the equivalent fuse_zero_partial_eof_folio()
and call it from the three paths that extend a file: a buffered write, a
size-extending setattr/truncate, and a size-extending fallocate
(fuse_write_update_attr(), fuse_do_setattr() and fuse_file_fallocate()).

writeback_cache connections are unaffected, as their writes go through
iomap_file_buffered_write(), which zeroes post-EOF folios.  The bug is
observable on a non-writeback_cache server that returns FOPEN_KEEP_CACHE on
writable files (without FOPEN_DIRECT_IO), and is caught by the new
write_extend_eof fuse selftest.

Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
 fs/fuse/dir.c    |  3 +++
 fs/fuse/file.c   | 56 ++++++++++++++++++++++++++++++++++++++++++++++++
 fs/fuse/fuse_i.h |  1 +
 3 files changed, 60 insertions(+)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 795e92037ce7..f6614ccef186 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -2282,6 +2282,9 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
 	 */
 	if ((is_truncate || !is_wb) &&
 	    S_ISREG(inode->i_mode) && oldsize != outarg.attr.size) {
+		if (outarg.attr.size > oldsize)
+			fuse_zero_partial_eof_folio(inode, oldsize,
+						    outarg.attr.size);
 		truncate_pagecache(inode, outarg.attr.size);
 		invalidate_inode_pages2(mapping);
 	}
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index cb8da4c06d17..a9063b4e9217 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -21,6 +21,8 @@
 #include <linux/splice.h>
 #include <linux/task_io_accounting_ops.h>
 #include <linux/iomap.h>
+#include <linux/highmem.h>
+#include <linux/rmap.h>
 
 static int fuse_send_open(struct fuse_mount *fm, u64 nodeid,
 			  unsigned int open_flags, int opcode,
@@ -1200,20 +1202,64 @@ static ssize_t fuse_send_write(struct fuse_io_args *ia, loff_t pos,
 	return err ?: ia->write.out.size;
 }
 
+/*
+ * An operation extended i_size past a non-folio-aligned old EOF at @from,
+ * turning [@from, @to) into a hole that must read back as zero.  If the old
+ * last folio is cached and was dirtied beyond the old EOF (e.g. mmap stores
+ * into the post-EOF region, which are undefined until the file grows), zero
+ * that tail so it is not exposed as stale data (xfstests generic/363).
+ *
+ * pagecache_isize_extended() cannot be used: it bails out for
+ * i_blocksize() >= PAGE_SIZE, and a non-fuseblk mount has
+ * s_blocksize == PAGE_SIZE, so the zeroing has to be done here.
+ * Callers hold i_rwsem, serialising this against concurrent writes and
+ * truncates; it must not run under fi->lock, as it locks the folio.
+ */
+void fuse_zero_partial_eof_folio(struct inode *inode, loff_t from, loff_t to)
+{
+	struct folio *folio;
+	size_t offset, end;
+
+	if (from >= to)
+		return;
+
+	folio = filemap_lock_folio(inode->i_mapping, from >> PAGE_SHIFT);
+	if (IS_ERR(folio))
+		return;
+
+	if (folio_mkclean(folio))
+		folio_mark_dirty(folio);
+
+	if (folio_test_dirty(folio)) {
+		offset = offset_in_folio(folio, from);
+		end = min_t(loff_t, to - folio_pos(folio), folio_size(folio));
+		folio_zero_segment(folio, offset, end);
+	}
+
+	folio_unlock(folio);
+	folio_put(folio);
+}
+
 bool fuse_write_update_attr(struct inode *inode, loff_t pos, ssize_t written)
 {
 	struct fuse_conn *fc = get_fuse_conn(inode);
 	struct fuse_inode *fi = get_fuse_inode(inode);
 	bool ret = false;
+	loff_t old_size = 0;
 
 	spin_lock(&fi->lock);
 	fi->attr_version = atomic64_inc_return(&fc->attr_version);
 	if (written > 0 && pos > inode->i_size) {
+		old_size = inode->i_size;
 		i_size_write(inode, pos);
 		ret = true;
 	}
 	spin_unlock(&fi->lock);
 
+	/* [old_size, pos - written) is the hole this write opened past EOF. */
+	if (ret)
+		fuse_zero_partial_eof_folio(inode, old_size, pos - written);
+
 	fuse_invalidate_attr_mask(inode, FUSE_STATX_MODSIZE);
 
 	return ret;
@@ -2913,8 +2959,18 @@ static long fuse_file_fallocate(struct file *file, int mode, loff_t offset,
 
 	/* we could have extended the file */
 	if (!(mode & FALLOC_FL_KEEP_SIZE)) {
+		loff_t oldsize = i_size_read(inode);
+
 		if (fuse_write_update_attr(inode, offset + length, length))
 			file_update_time(file);
+		/*
+		 * fuse_write_update_attr() already zeroes up to @offset when
+		 * the write started past the old EOF; this additionally covers
+		 * a fallocate whose range starts at or before it.  fallocate
+		 * writes no data, so the whole extension must read as zero; the
+		 * overlap is a no-op.
+		 */
+		fuse_zero_partial_eof_folio(inode, oldsize, offset + length);
 	}
 
 	if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_ZERO_RANGE))
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 85f738c53122..ee3b91b56fef 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1183,6 +1183,7 @@ long fuse_ioctl_common(struct file *file, unsigned int cmd,
 __poll_t fuse_file_poll(struct file *file, poll_table *wait);
 
 bool fuse_write_update_attr(struct inode *inode, loff_t pos, ssize_t written);
+void fuse_zero_partial_eof_folio(struct inode *inode, loff_t from, loff_t to);
 
 int fuse_flush_times(struct inode *inode, struct fuse_file *ff);
 int fuse_write_inode(struct inode *inode, struct writeback_control *wbc);
-- 
2.50.1


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* [PATCH 2/2] selftests/fuse: test post-EOF page zeroing when a file is extended
  2026-07-31 20:38 [PATCH 0/2] fuse: zero the partial EOF page when extending a file Jimmy Zuber
  2026-07-31 20:38 ` [PATCH 1/2] " Jimmy Zuber
@ 2026-07-31 20:38 ` Jimmy Zuber
  1 sibling, 0 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-07-31 20:38 UTC (permalink / raw)
  To: Miklos Szeredi, Shuah Khan; +Cc: fuse-devel, linux-kernel, linux-kselftest

Add a regression test for the bug where extending a file left the tail of
the old partial EOF page exposing stale mmap-dirtied data instead of zeros.

The test is a self-contained raw /dev/fuse server (no libfuse dependency)
that runs without writeback_cache and returns FOPEN_KEEP_CACHE, the
configuration in which the bug is visible.  Its backing data is always zero
in the hole, so any non-zero byte a read sees is stale page-cache data.
All offsets are relative to the runtime page size.  Four cases:

  - write_extend: pollute the post-EOF tail, extend past it by writing into
    a later page, and verify the tail reads back as zero;
  - ftruncate_extend: same, but extend via ftruncate();
  - fallocate_extend: same, but extend via fallocate() at the old EOF;
  - extend_into_eof_page_preserves_data: an extending write landing inside
    the old EOF page must not be clobbered by the zeroing.

Each case fails without the fix and passes with it.

Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
 .../selftests/filesystems/fuse/.gitignore     |   1 +
 .../selftests/filesystems/fuse/Makefile       |   3 +
 .../filesystems/fuse/write_extend_eof_test.c  | 368 ++++++++++++++++++
 3 files changed, 372 insertions(+)
 create mode 100644 tools/testing/selftests/filesystems/fuse/write_extend_eof_test.c

diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index 3e72e742d08e..fb51603fe419 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 fuse_mnt
 fusectl_test
+write_extend_eof_test
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index 612aad69a93a..0c2c613af0ac 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -3,10 +3,13 @@
 CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS := fusectl_test
+TEST_GEN_PROGS += write_extend_eof_test
 TEST_GEN_FILES := fuse_mnt
 
 include ../../lib.mk
 
+$(OUTPUT)/write_extend_eof_test: LDLIBS += -lpthread
+
 VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null)
 ifeq ($(VAR_CFLAGS),)
 VAR_CFLAGS := -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse
diff --git a/tools/testing/selftests/filesystems/fuse/write_extend_eof_test.c b/tools/testing/selftests/filesystems/fuse/write_extend_eof_test.c
new file mode 100644
index 000000000000..ca6ce6eca382
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/write_extend_eof_test.c
@@ -0,0 +1,368 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Regression test for the fuse write-extend partial-EOF-page zeroing bug.
+ *
+ * A buffered write that extends i_size past a non-page-aligned EOF must zero
+ * the tail of the old last page.  If an application has mmap'd that page and
+ * stored into the post-EOF region (undefined until the file grows), the
+ * now-in-bounds tail must read back as zero, not as the stale stored bytes.
+ *
+ * The bug is exposed on a non-writeback_cache server that keeps the page cache
+ * across the write (FOPEN_KEEP_CACHE without FOPEN_DIRECT_IO).  This test is a
+ * raw /dev/fuse server in that mode; the backing data is always zero in the
+ * hole, so any non-zero byte a read sees is stale page-cache data.
+ *
+ * Requires root to mount fuse.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/falloc.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <linux/fuse.h>
+
+#include "../../kselftest_harness.h"
+
+#define FUSE_ROOT_ID	1
+#define FILE_INO	2
+#define MAX_WRITE	(128 * 1024)
+#define BACKING_SIZE	(4 * 1024 * 1024)
+#define POLLUTE		0xee
+
+/* Server-side state, shared with the responder thread. */
+struct server {
+	int fd;
+	unsigned char backing[BACKING_SIZE];	/* authoritative bytes */
+	uint64_t size;
+};
+
+static void reply(int fd, uint64_t unique, int error, void *data, size_t len)
+{
+	struct fuse_out_header oh = {
+		.len = sizeof(oh) + (data ? len : 0),
+		.error = error,
+		.unique = unique,
+	};
+	struct iovec iov[2] = { { &oh, sizeof(oh) }, { data, len } };
+
+	/* Errors here are teardown races (device closed on unmount); ignore. */
+	if (writev(fd, iov, data ? 2 : 1) < 0)
+		return;
+}
+
+static void fill_attr(struct fuse_attr *a, uint64_t ino, uint32_t mode,
+		      uint64_t size)
+{
+	memset(a, 0, sizeof(*a));
+	a->ino = ino;
+	a->mode = mode;
+	a->nlink = 1;
+	a->size = size;
+	a->blksize = sysconf(_SC_PAGESIZE);
+}
+
+static void *server_thread(void *arg)
+{
+	struct server *s = arg;
+	static char buf[MAX_WRITE + 4096];
+
+	for (;;) {
+		ssize_t n = read(s->fd, buf, sizeof(buf));
+		struct fuse_in_header *ih = (void *)buf;
+
+		if (n < 0) {
+			if (errno == EINTR || errno == EAGAIN)
+				continue;
+			return NULL;	/* device closed on unmount */
+		}
+		if (n < (ssize_t)sizeof(*ih))
+			continue;
+
+		switch (ih->opcode) {
+		case FUSE_INIT: {
+			struct fuse_init_in *in = (void *)(ih + 1);
+			struct fuse_init_out out = {0};
+
+			/* No FUSE_WRITEBACK_CACHE: the exposed configuration. */
+			out.major = FUSE_KERNEL_VERSION;
+			out.minor = FUSE_KERNEL_MINOR_VERSION;
+			out.max_readahead = in->max_readahead;
+			out.max_write = MAX_WRITE;
+			out.max_background = 16;
+			out.congestion_threshold = 12;
+			out.flags = FUSE_MAX_PAGES;
+			out.max_pages = MAX_WRITE / sysconf(_SC_PAGESIZE);
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_GETATTR: {
+			struct fuse_attr_out out = {0};
+			int root = ih->nodeid == FUSE_ROOT_ID;
+
+			out.attr_valid = 3600;
+			fill_attr(&out.attr, ih->nodeid,
+				  root ? (S_IFDIR | 0755) : (S_IFREG | 0644),
+				  root ? 0 : s->size);
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_LOOKUP: {
+			struct fuse_entry_out out = {0};
+
+			out.nodeid = FILE_INO;
+			out.attr_valid = 3600;
+			out.entry_valid = 3600;
+			fill_attr(&out.attr, FILE_INO, S_IFREG | 0644, s->size);
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_OPEN:
+		case FUSE_OPENDIR: {
+			struct fuse_open_out out = {0};
+
+			/* Keep the cache across the write, but not direct I/O. */
+			out.open_flags = FOPEN_KEEP_CACHE;
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_READ: {
+			struct fuse_read_in *in = (void *)(ih + 1);
+			uint64_t off = in->offset;
+			uint32_t size = in->size;
+
+			if (off >= BACKING_SIZE)
+				size = 0;
+			else if (off + size > BACKING_SIZE)
+				size = BACKING_SIZE - off;
+			reply(s->fd, ih->unique, 0, s->backing + off, size);
+			break;
+		}
+		case FUSE_WRITE: {
+			struct fuse_write_in *in = (void *)(ih + 1);
+			struct fuse_write_out out = {0};
+			uint64_t off = in->offset;
+			uint32_t size = in->size;
+
+			if (off < BACKING_SIZE) {
+				uint32_t c = size;
+
+				if (off + c > BACKING_SIZE)
+					c = BACKING_SIZE - off;
+				memcpy(s->backing + off, in + 1, c);
+				if (off + c > s->size)
+					s->size = off + c;
+			}
+			out.size = size;
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_SETATTR: {
+			struct fuse_setattr_in *in = (void *)(ih + 1);
+			struct fuse_attr_out out = {0};
+
+			if ((in->valid & FATTR_SIZE) && in->size <= BACKING_SIZE) {
+				if (in->size > s->size)
+					memset(s->backing + s->size, 0,
+					       in->size - s->size);
+				s->size = in->size;
+			}
+			out.attr_valid = 3600;
+			fill_attr(&out.attr, ih->nodeid, S_IFREG | 0644, s->size);
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_FALLOCATE: {
+			struct fuse_fallocate_in *in = (void *)(ih + 1);
+			uint64_t end = in->offset + in->length;
+
+			/* Only plain (size-extending) fallocate is used here. */
+			if (!(in->mode & FALLOC_FL_KEEP_SIZE) &&
+			    end <= BACKING_SIZE && end > s->size) {
+				memset(s->backing + s->size, 0, end - s->size);
+				s->size = end;
+			}
+			reply(s->fd, ih->unique, 0, NULL, 0);
+			break;
+		}
+		case FUSE_FLUSH:
+		case FUSE_RELEASE:
+		case FUSE_RELEASEDIR:
+		case FUSE_FSYNC:
+		case FUSE_ACCESS:
+			reply(s->fd, ih->unique, 0, NULL, 0);
+			break;
+		case FUSE_FORGET:
+			break;
+		default:
+			reply(s->fd, ih->unique, -EOPNOTSUPP, NULL, 0);
+			break;
+		}
+	}
+}
+
+FIXTURE(fuse)
+{
+	struct server *srv;
+	pthread_t thread;
+	char dir[64];
+	long page;		/* runtime page size */
+	off_t eof;		/* mid-page EOF, page-relative */
+	int fd;			/* open test file */
+	char *map;		/* mmap of the EOF page */
+	int mounted;
+};
+
+FIXTURE_SETUP(fuse)
+{
+	char opts[128];
+	pthread_t t;
+
+	if (geteuid() != 0)
+		SKIP(return, "need root to mount fuse");
+
+	self->page = sysconf(_SC_PAGESIZE);
+	self->fd = -1;
+	self->map = MAP_FAILED;
+
+	self->srv = mmap(NULL, sizeof(*self->srv), PROT_READ | PROT_WRITE,
+			 MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(MAP_FAILED, self->srv);
+
+	self->srv->fd = open("/dev/fuse", O_RDWR);
+	ASSERT_GE(self->srv->fd, 0);
+
+	strcpy(self->dir, "/tmp/fuse_weof_XXXXXX");
+	ASSERT_NE(NULL, mkdtemp(self->dir));
+
+	snprintf(opts, sizeof(opts),
+		 "fd=%d,rootmode=40000,user_id=0,group_id=0",
+		 self->srv->fd);
+	ASSERT_EQ(0, mount("fuse", self->dir, "fuse", 0, opts));
+	self->mounted = 1;
+
+	ASSERT_EQ(0, pthread_create(&t, NULL, server_thread, self->srv));
+	self->thread = t;
+}
+
+FIXTURE_TEARDOWN(fuse)
+{
+	if (self->map != MAP_FAILED)
+		munmap(self->map, self->page);
+	if (self->fd >= 0)
+		close(self->fd);
+	if (self->mounted)
+		umount2(self->dir, MNT_DETACH);
+	if (self->srv && self->srv != MAP_FAILED) {
+		if (self->srv->fd > 0)
+			close(self->srv->fd);
+		munmap(self->srv, sizeof(*self->srv));
+	}
+	if (self->dir[0])
+		rmdir(self->dir);
+}
+
+/*
+ * Create the test file with a mid-page EOF and mmap-store POLLUTE into its
+ * post-EOF tail (a legal store, undefined until the file grows).  Leaves the
+ * file open and the EOF page mapped in the fixture for the caller to extend.
+ */
+static void pollute_eof_tail(struct __test_metadata *_metadata,
+			     FIXTURE_DATA(fuse) * self)
+{
+	off_t eof = 2 * self->page + self->page / 4;
+	char path[128];
+	char *buf;
+
+	snprintf(path, sizeof(path), "%s/file", self->dir);
+	self->fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);
+	ASSERT_GE(self->fd, 0);
+	self->eof = eof;
+
+	buf = malloc(eof);
+	ASSERT_NE(NULL, buf);
+	memset(buf, 'A', eof);
+	ASSERT_EQ(eof, pwrite(self->fd, buf, eof, 0));
+	free(buf);
+
+	self->map = mmap(NULL, self->page, PROT_READ | PROT_WRITE, MAP_SHARED,
+			 self->fd, eof & ~(self->page - 1));
+	ASSERT_NE(MAP_FAILED, self->map);
+	memset(self->map + (eof & (self->page - 1)), POLLUTE,
+	       self->page - (eof & (self->page - 1)));
+}
+
+/* Assert the old post-EOF tail [eof, end of its page) now reads back as zero. */
+static void assert_tail_zeroed(struct __test_metadata *_metadata,
+			       FIXTURE_DATA(fuse) * self)
+{
+	off_t base = self->eof & ~(self->page - 1);
+	char *tail = malloc(self->page);
+	int i;
+
+	ASSERT_NE(NULL, tail);
+	ASSERT_EQ(self->page, pread(self->fd, tail, self->page, base));
+	for (i = self->eof & (self->page - 1); i < self->page; i++)
+		ASSERT_EQ(0, tail[i]);
+	free(tail);
+}
+
+/* Basic: pollute the post-EOF tail, extend past it by a later write. */
+TEST_F(fuse, write_extend)
+{
+	pollute_eof_tail(_metadata, self);
+	ASSERT_EQ(4, pwrite(self->fd, "data", 4, 5 * self->page + self->page / 3));
+	assert_tail_zeroed(_metadata, self);
+}
+
+/* Extend via ftruncate() rather than a write. */
+TEST_F(fuse, ftruncate_extend)
+{
+	pollute_eof_tail(_metadata, self);
+	ASSERT_EQ(0, ftruncate(self->fd, 8 * self->page));
+	assert_tail_zeroed(_metadata, self);
+}
+
+/* Extend via fallocate() starting at the old EOF. */
+TEST_F(fuse, fallocate_extend)
+{
+	pollute_eof_tail(_metadata, self);
+	ASSERT_EQ(0, fallocate(self->fd, 0, self->eof, 4 * self->page));
+	assert_tail_zeroed(_metadata, self);
+}
+
+/* A write landing inside the old EOF page must not clobber its own data. */
+TEST_F(fuse, extend_into_eof_page_preserves_data)
+{
+	off_t base, wr;
+	char *buf, *rd;
+	int i;
+
+	pollute_eof_tail(_metadata, self);
+	base = self->eof & ~(self->page - 1);
+	wr = base + 3 * self->page / 4;		/* starts in the EOF page */
+
+	buf = malloc(2 * self->page);
+	ASSERT_NE(NULL, buf);
+	memset(buf, 'B', 2 * self->page);
+	ASSERT_EQ(2 * self->page, pwrite(self->fd, buf, 2 * self->page, wr));
+	free(buf);
+
+	rd = malloc(self->page);
+	ASSERT_NE(NULL, rd);
+	ASSERT_EQ(self->page, pread(self->fd, rd, self->page, base));
+	/* [eof, wr) is hole -> zero; [wr, page) is written data -> 'B'. */
+	for (i = self->eof & (self->page - 1); i < wr - base; i++)
+		ASSERT_EQ(0, rd[i]);
+	for (i = wr - base; i < self->page; i++)
+		ASSERT_EQ('B', rd[i]);
+	free(rd);
+}
+
+TEST_HARNESS_MAIN
-- 
2.50.1


^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-07-31 20:39 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31 20:38 [PATCH 0/2] fuse: zero the partial EOF page when extending a file Jimmy Zuber
2026-07-31 20:38 ` [PATCH 1/2] " Jimmy Zuber
2026-07-31 20:38 ` [PATCH 2/2] selftests/fuse: test post-EOF page zeroing when a file is extended Jimmy Zuber

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).