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 <jack@suse.cz>
Link: https://patch.msgid.link/20260727104923.3828017-37-jack@suse.cz
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Jan Kara
2026-07-27 12:49:35 +02:00
committed by Christian Brauner
parent 84af7c3b34
commit c26339e1df
4 changed files with 76 additions and 58 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -5799,6 +5799,10 @@ static int ext4_do_update_inode(handle_t *handle,
* 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;
err = ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal,
EXT4_I(inode)->i_sync_tid);
} else {
struct ext4_iloc iloc;
err = __ext4_get_inode_loc_noinmem(inode, &iloc);
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);
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;
return ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal,
EXT4_I(inode)->i_sync_tid);
}
int ext4_sync_inode_metadata(struct inode *inode, struct writeback_control *wbc)
{
struct ext4_iloc iloc;
struct mapping_metadata_bhs *mmb;
int err;
/* 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)
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;
}

View File

@@ -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,