git: 26d84bb8bf9b - main - ufs: flush soft updates dependencies for O_SYNC and O_DSYNC writes

From: Maxim Sobolev <sobomax_at_FreeBSD.org>
Date: Tue, 13 Oct 2026 04:29:54 UTC
The branch main has been updated by sobomax:

URL: https://cgit.FreeBSD.org/src/commit/?id=26d84bb8bf9b4acd0aa80b0208c1ba4fc9646ff3

commit 26d84bb8bf9b4acd0aa80b0208c1ba4fc9646ff3
Author:     Maxim Sobolev <sobomax@FreeBSD.org>
AuthorDate: 2026-10-13 02:42:31 +0000
Commit:     Maxim Sobolev <sobomax@FreeBSD.org>
CommitDate: 2026-10-13 04:28:32 +0000

    ufs: flush soft updates dependencies for O_SYNC and O_DSYNC writes
    
    For a synchronous write, ffs_write() writes the data buffers with
    bwrite() and then the inode with ffs_update(vp, 1).  With soft updates,
    a new block pointer can only reach the disk after the dependencies of
    the block, such as the cylinder group map that allocates it, are
    written.  Until then initiate_write_inodeblock_ufs[12]() and
    initiate_write_indirdep() roll the pointer, and for an extending write
    the size, back in the buffer being written.  Nothing in the write path
    flushes those dependencies, so the inode went out with the old size and
    pointers, ffs_update() cleared IN_SIZEMOD and IN_IBLKDATA, and a crash
    before the syncer rewrote the inode lost data that O_SYNC or O_DSYNC
    promised was on stable storage.  The same held for a new block below an
    indirect block, which ffs_balloc() writes synchronously but with the
    pointer rolled back.
    
    When soft updates are enabled and the write allocated blocks, finish it
    with ffs_syncvnode(), as fsync(2) and fdatasync(2) do: it flushes the
    dependencies, rewrites the rolled back indirect blocks and writes the
    inode, or for O_DSYNC only writes the inode when IN_SIZEMOD or
    IN_IBLKDATA say it is needed.  The flags cannot tell whether the write
    allocated anything, because allocating a block below an existing
    indirect block sets neither, so compare the inode's block count before
    and after the write instead.  A write that allocated nothing, such as
    an overwrite of existing blocks, adds no dependencies of its own and
    keeps writing only the inode, so that it does not pay for a sync of the
    whole vnode.  ffs_syncvnode() only drops the vnode lock when it has to
    resolve directory dependencies, which ffs_write() never sees.
    
    ffs_syncvnode() also writes whatever other descriptors left dirty on
    the vnode, so the first synchronous write after a lot of buffered
    writes would pay for all of them.  To put that cost on the open
    instead, where it is less unexpected for a descriptor opened for
    synchronous writes, add ffs_open() and, with soft updates, sync the
    vnode there when a regular file is opened for writing with O_SYNC or
    O_DSYNC, the same way the writes would: a full sync for O_SYNC, a
    data-only one for O_DSYNC.  It calls ffs_syncvnode() rather than
    VOP_FSYNC(), whose softdep_fsync() may drop the vnode lock in the middle
    of the open.  The sync on open only moves work that the writes would
    otherwise do, so it is skipped rather than waited for when writes are
    suspended, and its errors do not fail the open.  Without soft updates
    the writes sync nothing but their own blocks and the inode, so there is
    nothing to move and the open does not sync.
    
    Sponsored by:   Sippy Software, Inc.
    Reviewed by:    kib
    Tested by:      pho
    Differential Revision:  https://reviews.freebsd.org/D60134
    MFC after:      1 month
---
 sys/ufs/ffs/ffs_vnops.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 59 insertions(+), 1 deletion(-)

diff --git a/sys/ufs/ffs/ffs_vnops.c b/sys/ufs/ffs/ffs_vnops.c
index 4044ecf20f92..493936578095 100644
--- a/sys/ufs/ffs/ffs_vnops.c
+++ b/sys/ufs/ffs/ffs_vnops.c
@@ -71,6 +71,7 @@
 #include <sys/buf.h>
 #include <sys/conf.h>
 #include <sys/extattr.h>
+#include <sys/fcntl.h>
 #include <sys/kernel.h>
 #include <sys/limits.h>
 #include <sys/malloc.h>
@@ -117,6 +118,7 @@ static vop_lock1_t	ffs_lock;
 #ifdef INVARIANTS
 static vop_unlock_t	ffs_unlock_debug;
 #endif
+static vop_open_t	ffs_open;
 static vop_read_t	ffs_read;
 static vop_write_t	ffs_write;
 static int	ffs_extread(struct vnode *vp, struct uio *uio, int ioflag);
@@ -145,6 +147,7 @@ struct vop_vector ffs_vnodeops1 = {
 #ifdef INVARIANTS
 	.vop_unlock =		ffs_unlock_debug,
 #endif
+	.vop_open =		ffs_open,
 	.vop_read =		ffs_read,
 	.vop_reallocblks =	ffs_reallocblks,
 	.vop_write =		ffs_write,
@@ -180,6 +183,7 @@ struct vop_vector ffs_vnodeops2 = {
 #ifdef INVARIANTS
 	.vop_unlock =		ffs_unlock_debug,
 #endif
+	.vop_open =		ffs_open,
 	.vop_read =		ffs_read,
 	.vop_reallocblks =	ffs_reallocblks,
 	.vop_write =		ffs_write,
@@ -455,6 +459,41 @@ ffs_fdatasync(struct vop_fdatasync_args *ap)
 	return (ffs_syncvnode(ap->a_vp, MNT_WAIT, DATA_ONLY));
 }
 
+/*
+ * With soft updates, an O_SYNC or O_DSYNC write that allocates blocks
+ * finishes with ffs_syncvnode(), which also writes whatever other
+ * descriptors left dirty on the vnode.  Sync the vnode when it is opened
+ * for writing with either flag, so that this cost is mostly paid once, by
+ * the open, and not by the first synchronous write.
+ *
+ * This only moves work that the writes would otherwise do, so skip it
+ * rather than wait when writes are suspended, and do not fail the open on
+ * its errors.  Without soft updates the writes sync nothing but their own
+ * blocks and the inode, so there is nothing to move and the open does not
+ * sync.  A writable open locks the vnode exclusively.
+ */
+static int
+ffs_open(struct vop_open_args *ap)
+{
+	struct vnode *vp;
+	struct mount *mp;
+	int error;
+
+	error = VOP_OPEN_APV(&ufs_vnodeops, ap);
+	vp = ap->a_vp;
+	if (error != 0 || vp->v_type != VREG || !DOINGSOFTDEP(vp) ||
+	    (ap->a_mode & FWRITE) == 0 ||
+	    (ap->a_mode & (FFSYNC | FDSYNC)) == 0)
+		return (error);
+	ASSERT_VOP_ELOCKED(vp, "ffs_open");
+	if (vn_start_write(vp, &mp, V_NOWAIT) != 0)
+		return (0);
+	(void)ffs_syncvnode(vp, MNT_WAIT,
+	    (ap->a_mode & FFSYNC) != 0 ? 0 : DATA_ONLY);
+	vn_finished_write(mp);
+	return (0);
+}
+
 static int
 ffs_lock(
 	struct vop_lock1_args /* {
@@ -822,6 +861,7 @@ ffs_write(
 	struct buf *bp;
 	ufs_lbn_t lbn;
 	off_t osize;
+	uint64_t oblocks;
 	ssize_t resid, r;
 	int seqcount;
 	int blkoffset, error, flags, ioflag, size, xfersize;
@@ -885,6 +925,7 @@ ffs_write(
 
 	resid = uio->uio_resid;
 	osize = ip->i_size;
+	oblocks = DIP(ip, i_blocks);
 	if (seqcount > BA_SEQMAX)
 		flags = BA_SEQMAX << BA_SEQSHIFT;
 	else
@@ -1023,7 +1064,24 @@ ffs_write(
 			uio->uio_resid = resid;
 		}
 	} else if (resid > uio->uio_resid && (ioflag & IO_SYNC)) {
-		if (!(ioflag & IO_DATASYNC) ||
+		if (DOINGSOFTDEP(vp) && DIP(ip, i_blocks) != oblocks) {
+			/*
+			 * With soft updates, new block pointers in the
+			 * inode and in indirect blocks are rolled back
+			 * when those are written before the dependencies
+			 * of the new blocks, such as the cylinder group
+			 * maps, are on disk.  If this write allocated
+			 * blocks, let ffs_syncvnode() flush the
+			 * dependencies first.  It only drops the vnode
+			 * lock for directories.  A write that allocated
+			 * nothing adds no dependencies of its own and only
+			 * needs the inode written, as without soft updates.
+			 */
+			error = ffs_syncvnode(vp, MNT_WAIT,
+			    (ioflag & IO_DATASYNC) != 0 ? DATA_ONLY : 0);
+			KASSERT(error != ERELOOKUP,
+			    ("ffs_write: vnode %p unlocked by sync", vp));
+		} else if (!(ioflag & IO_DATASYNC) ||
 		    (ip->i_flag & (IN_SIZEMOD | IN_IBLKDATA)))
 			error = ffs_update(vp, 1);
 		if (ffs_fsfail_cleanup(VFSTOUFS(vp->v_mount), error))