All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 0/2] fuse: zero the partial EOF page when extending a file
@ 2026-08-20 12:26 Jimmy Zuber
  2026-08-20 12:26 ` [PATCH v2 1/2] " Jimmy Zuber
  2026-08-20 12:26 ` [PATCH v2 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-08-20 12:26 UTC (permalink / raw)
  To: miklos, --to=shuah
  Cc: fuse-devel, --cc=linux-kernel, --cc=linux-kselftest, --cc=jack,
	--cc=joannelkoong, --cc=bfoster

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.

Some file systems get this zeroing automatically at writeback time
(block_write_full_folio() / iomap_writeback_handle_eof() zero the tail of the
folio straddling i_size).  A non-writeback caching fuse file system uses neither
path, so it has to zero the tail itself from the size-extending paths, like
XFS (xfs_file_write_zero_eof()) and ext4 (ext4_block_zero_eof()) do.
pagecache_isize_extended() cannot be reused: it is a no-op when
i_blocksize() >= PAGE_SIZE, and would increase the work in that function
for use cases that don't need it, if changed to support this situation.

Patch 1 adds fuse_zero_partial_eof_folio() and calls it up front from the
three paths that extend a file: buffered write, setattr, and
fallocate.  Patch 2 adds a self-contained raw /dev/fuse selftest covering each
path.

Tested by booting the patched kernel under User-Mode Linux and running the new
selftest, and reproduced against libfuse's passthrough_ll example daemon.

Changes since v1 [1]:
- Miklos and Jan Kara discussed whether there was a better pattern with less
  duplication between file systems.  Jan confirmed that other file systems do
  this zeroing themselves from their write/setattr paths (e.g. XFS
  xfs_file_write_zero_eof(), ext4 ext4_block_zero_eof()), because the block /
  iomap writeback that would otherwise zero the straddling folio does not run
  on this path.  File systems currently implement this separately.  It is
  beyond me to pursue unifying it.  Restructure to match the pattern in other
  file systems: zero [old EOF, write start) up front from fuse_perform_write()
  rather than after the fact from fuse_write_update_attr(), keyed on the write
  starting beyond EOF.
- This keeps the zeroed range disjoint from the written data, so a write that
  lands inside the old EOF folio is preserved by construction; the v1
  "pos - written" arithmetic and the fallocate double-call are gone.
- fuse_write_update_attr() goes back to being a pure attr update; the
  setattr/truncate call in fuse_do_setattr() is unchanged.
- The selftest is unchanged from v1.

[1] https://lore.kernel.org/all/20260731203842.540798-1-jamz@amazon.com/

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 v2 1/2] fuse: zero the partial EOF page when extending a file
  2026-08-20 12:26 [PATCH v2 0/2] fuse: zero the partial EOF page when extending a file Jimmy Zuber
@ 2026-08-20 12:26 ` Jimmy Zuber
  2026-08-20 12:26 ` [PATCH v2 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-08-20 12:26 UTC (permalink / raw)
  To: miklos, --to=shuah
  Cc: fuse-devel, --cc=linux-kernel, --cc=linux-kselftest, --cc=jack,
	--cc=joannelkoong, --cc=bfoster

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.

Some file systems get this zeroing automatically at writeback time
(block_write_full_folio() / iomap_writeback_handle_eof() zero the tail of the
folio straddling i_size).  A non-writeback caching fuse file system uses neither
path, so it has to zero the tail itself from the size-extending paths, like
XFS (xfs_file_write_zero_eof()) and ext4 (ext4_block_zero_eof()) do.
pagecache_isize_extended() cannot be reused: it is a no-op when
i_blocksize() >= PAGE_SIZE, and would increase the work in that function
for use cases that don't need it, if changed to support this situation.

Add fuse_zero_partial_eof_folio(), which zeroes the tail of the old EOF
folio, and call it up front from the three paths that extend a file,
mirroring xfs_file_write_zero_eof() and ext4_block_zero_eof():

  - a buffered write whose position is past the old EOF (fuse_perform_write(),
    before the page cache is filled);
  - a size-extending setattr/truncate (fuse_do_setattr());
  - a size-extending fallocate (fuse_file_fallocate()).

Zeroing [old EOF, write start) before the write, rather than after it, keeps
the zeroed range disjoint from the written data, so a write that lands inside
the old EOF folio is preserved without special-casing.

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..57630ad0af66 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,6 +1202,43 @@ static ssize_t fuse_send_write(struct fuse_io_args *ia, loff_t pos,
 	return err ?: ia->write.out.size;
 }
 
+/*
+ * A size-extending operation is about to turn [@from, @to) -- the range past a
+ * non-folio-aligned old EOF at @from -- 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 instead of
+ * zeros (xfstests generic/363).  Only the folio straddling @from can hold such
+ * bytes, so a single folio is handled, as in pagecache_isize_extended().
+ *
+ * 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);
@@ -1368,9 +1407,19 @@ static ssize_t fuse_perform_write(struct kiocb *iocb, struct iov_iter *ii)
 	struct fuse_conn *fc = get_fuse_conn(inode);
 	struct fuse_inode *fi = get_fuse_inode(inode);
 	loff_t pos = iocb->ki_pos;
+	loff_t old_size = i_size_read(inode);
 	int err = 0;
 	ssize_t res = 0;
 
+	/*
+	 * If the write starts past a non-aligned EOF, zero the old EOF folio's
+	 * tail before filling the page cache, so [old_size, pos) reads as the
+	 * hole it is.  The write below fills from @pos, disjoint from this
+	 * range.
+	 */
+	if (pos > old_size)
+		fuse_zero_partial_eof_folio(inode, old_size, pos);
+
 	if (inode->i_size < pos + iov_iter_count(ii))
 		set_bit(FUSE_I_SIZE_UNSTABLE, &fi->state);
 
@@ -2913,6 +2962,13 @@ 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)) {
+		/*
+		 * fallocate writes no data, so the whole extension past the old
+		 * EOF is a hole; zero the old EOF folio's tail before publishing
+		 * the new size.
+		 */
+		fuse_zero_partial_eof_folio(inode, i_size_read(inode),
+					    offset + length);
 		if (fuse_write_update_attr(inode, offset + length, length))
 			file_update_time(file);
 	}
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 v2 2/2] selftests/fuse: test post-EOF page zeroing when a file is extended
  2026-08-20 12:26 [PATCH v2 0/2] fuse: zero the partial EOF page when extending a file Jimmy Zuber
  2026-08-20 12:26 ` [PATCH v2 1/2] " Jimmy Zuber
@ 2026-08-20 12:26 ` Jimmy Zuber
  1 sibling, 0 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-08-20 12:26 UTC (permalink / raw)
  To: miklos, --to=shuah
  Cc: fuse-devel, --cc=linux-kernel, --cc=linux-kselftest, --cc=jack,
	--cc=joannelkoong, --cc=bfoster

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-08-20 12:27 UTC | newest]

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

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.