From c26339e1df335423bcbb83d6fa6ff94b1545b8ed Mon Sep 17 00:00:00 2001 From: Jan Kara Date: Mon, 27 Jul 2026 12:49:35 +0200 Subject: [PATCH] ext4: Fix data integrity writeout issues in nojournal mode Several racing fsyncs on ext4 in nojournal mode could result in some fsync returning earlier than all metadata buffers were properly persisted. Also ext4_fsync() in nojournal mode was somewhat inefficient because it was always writing out the inode regardless whether it was dirty or not. Fix these issues by using new .sync_inode_metadata method which makes sure all inode related metadata is written to disk during any WB_SYNC_ALL writeback in nojournal mode. This also somewhat simplifies the nojournal mode fsync handling. Signed-off-by: Jan Kara Link: https://patch.msgid.link/20260727104923.3828017-37-jack@suse.cz Signed-off-by: Christian Brauner (Amutable) --- fs/ext4/ext4.h | 1 + fs/ext4/fsync.c | 28 +++------------ fs/ext4/inode.c | 90 ++++++++++++++++++++++++++++++++----------------- fs/ext4/super.c | 7 +++- 4 files changed, 72 insertions(+), 54 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 64f8f63f4415..0f06155a35a6 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3166,6 +3166,7 @@ extern struct inode *__ext4_iget(struct super_block *sb, unsigned long ino, __ext4_iget((sb), (ino), (flags), __func__, __LINE__) extern int ext4_write_inode(struct inode *, struct writeback_control *); +extern int ext4_sync_inode_metadata(struct inode *, struct writeback_control *); extern int ext4_setattr(struct mnt_idmap *, struct dentry *, struct iattr *); extern u32 ext4_dio_alignment(struct inode *inode); diff --git a/fs/ext4/fsync.c b/fs/ext4/fsync.c index b7ea4433f4be..2999c2cc8fcf 100644 --- a/fs/ext4/fsync.c +++ b/fs/ext4/fsync.c @@ -46,7 +46,6 @@ static int ext4_sync_parent(struct inode *inode) { struct dentry *dentry, *next; - struct mapping_metadata_bhs *mmb; int ret = 0; if (!ext4_test_inode_state(inode, EXT4_STATE_NEWENTRY)) @@ -69,12 +68,6 @@ static int ext4_sync_parent(struct inode *inode) * through ext4_evict_inode()) and so we are safe to flush * metadata blocks and the inode. */ - mmb = ext4_i_metadata_bhs(inode); - if (mmb) { - ret = mmb_sync(mmb); - if (ret) - break; - } ret = sync_inode_metadata(inode, 1); if (ret) break; @@ -87,22 +80,11 @@ static int ext4_fsync_nojournal(struct file *file, loff_t start, loff_t end, int datasync, bool *needs_barrier) { struct inode *inode = file->f_inode; - struct writeback_control wbc = { - .sync_mode = WB_SYNC_ALL, - .nr_to_write = 0, - }; int ret; - ret = mmb_fsync_noflush(file, ext4_i_metadata_bhs(inode), - start, end, datasync); + ret = sync_inode_metadata(inode, 1); if (ret) return ret; - - /* Force writeout of inode table buffer to disk */ - ret = ext4_write_inode(inode, &wbc); - if (ret) - return ret; - ret = ext4_sync_parent(inode); if (test_opt(inode->i_sb, BARRIER)) @@ -160,6 +142,10 @@ int ext4_sync_file(struct file *file, loff_t start, loff_t end, int datasync) if (sb_rdonly(inode->i_sb)) goto out; + ret = file_write_and_wait_range(file, start, end); + if (ret) + goto out; + if (!EXT4_SB(inode->i_sb)->s_journal) { ret = ext4_fsync_nojournal(file, start, end, datasync, &needs_barrier); @@ -168,10 +154,6 @@ int ext4_sync_file(struct file *file, loff_t start, loff_t end, int datasync) goto out; } - ret = file_write_and_wait_range(file, start, end); - if (ret) - goto out; - /* * The caller's filemap_fdatawrite()/wait will sync the data. * Metadata is in the journal, we wait for proper transaction to diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index e6acef486ee1..7a1f961cd11c 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -5799,6 +5799,10 @@ out_brelse: * ext4_mark_inode_dirty(). This is a correctness thing for WB_SYNC_ALL * writeback. * + * For nojournal mode all the work is done in ext4_sync_inode_metadata() + * because inode content is already copied into raw inode buffer and inode + * is marked with I_METADATA_WRITEBACK. + * * Note that we are absolutely dependent upon all inode dirtiers doing the * right thing: they *must* call mark_inode_dirty() after dirtying info in * which we are interested. @@ -5824,42 +5828,54 @@ int ext4_write_inode(struct inode *inode, struct writeback_control *wbc) if (unlikely(err)) return err; - if (EXT4_SB(inode->i_sb)->s_journal) { - if (ext4_journal_current_handle()) { - ext4_debug("called recursively, non-PF_MEMALLOC!\n"); - dump_stack(); - return -EIO; - } + if (!EXT4_SB(inode->i_sb)->s_journal) + return 0; - /* - * No need to force transaction in WB_SYNC_NONE mode. Also - * ext4_sync_fs() will force the commit after everything is - * written. - */ - if (wbc->sync_mode != WB_SYNC_ALL || wbc->for_sync) - return 0; + if (ext4_journal_current_handle()) { + ext4_debug("called recursively, non-PF_MEMALLOC!\n"); + dump_stack(); + return -EIO; + } + + /* + * No need to force transaction in WB_SYNC_NONE mode. Also + * ext4_sync_fs() will force the commit after everything is + * written. + */ + if (wbc->sync_mode != WB_SYNC_ALL || wbc->for_sync) + return 0; - err = ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal, + return ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal, EXT4_I(inode)->i_sync_tid); - } else { - struct ext4_iloc iloc; +} + +int ext4_sync_inode_metadata(struct inode *inode, struct writeback_control *wbc) +{ + struct ext4_iloc iloc; + struct mapping_metadata_bhs *mmb; + int err; - err = __ext4_get_inode_loc_noinmem(inode, &iloc); + /* We should only get here in nojournal mode */ + if (WARN_ON_ONCE(EXT4_SB(inode->i_sb)->s_journal)) + return -EFSCORRUPTED; + + err = __ext4_get_inode_loc_noinmem(inode, &iloc); + if (err) + return err; + mmb = READ_ONCE(EXT4_I(inode)->i_metadata_bhs); + if (mmb) { + err = mmb_sync(mmb); if (err) - return err; - /* - * sync(2) will flush the whole buffer cache. No need to do - * it here separately for each inode. - */ - if (wbc->sync_mode == WB_SYNC_ALL && !wbc->for_sync) - sync_dirty_buffer(iloc.bh); - if (buffer_req(iloc.bh) && !buffer_uptodate(iloc.bh)) { - ext4_error_inode_block(inode, iloc.bh->b_blocknr, EIO, - "IO error syncing inode"); - err = -EIO; - } - brelse(iloc.bh); + goto out; } + sync_dirty_buffer(iloc.bh); + if (buffer_write_io_error(iloc.bh)) { + ext4_error_inode_block(inode, iloc.bh->b_blocknr, EIO, + "IO error syncing inode"); + err = -EIO; + } +out: + brelse(iloc.bh); return err; } @@ -6407,6 +6423,20 @@ int ext4_mark_iloc_dirty(handle_t *handle, /* ext4_do_update_inode() does jbd2_journal_dirty_metadata */ err = ext4_do_update_inode(handle, inode, iloc); put_bh(iloc->bh); + /* + * Mark that there's metadata writeout pending for the inode so that it + * gets properly flushed on fsync(2) and similar. + */ + if (!EXT4_SB(inode->i_sb)->s_journal) { + /* + * Inode didn't need to go through dirtying, make sure it is + * attached to wb so that writeback can handle it. + */ + spin_lock(&inode->i_lock); + inode_attach_wb(inode, NULL); + spin_unlock(&inode->i_lock); + set_inode_metadata_writeback(inode); + } return err; } diff --git a/fs/ext4/super.c b/fs/ext4/super.c index 8671fa1209dd..ae33f5bcb133 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -1608,9 +1608,13 @@ static int ext4_nfs_commit_metadata(struct inode *inode) struct writeback_control wbc = { .sync_mode = WB_SYNC_ALL }; + int ret; trace_ext4_nfs_commit_metadata(inode); - return ext4_write_inode(inode, &wbc); + ret = ext4_write_inode(inode, &wbc); + if (!ret && inode_state_read_once(inode) & I_METADATA_WRITEBACK) + ret = ext4_sync_inode_metadata(inode, &wbc); + return ret; } #ifdef CONFIG_QUOTA @@ -1667,6 +1671,7 @@ static const struct super_operations ext4_sops = { .free_inode = ext4_free_in_core_inode, .destroy_inode = ext4_destroy_inode, .write_inode = ext4_write_inode, + .sync_inode_metadata = ext4_sync_inode_metadata, .dirty_inode = ext4_dirty_inode, .drop_inode = ext4_drop_inode, .evict_inode = ext4_evict_inode, -- 2.47.3