[PATCH 06/10] ovl: mark the overlay inode dirty on modification
From: Josef Bacik
Date: Tue Oct 27 2026 - 17:19:31 EST
To sync only what was written through one overlay we have to know which
upper inodes that is. Mark the overlay inode dirty whenever the upper
inode is changed through it, which puts it on the dirty list of the
overlay's own bdi.
This is about data and timestamps. Other metadata is left to ->sync_fs()
of the upper filesystem, and nothing is marked unless that has
SB_I_OVL_USE_SYNC_FS.
Nearly every change already ends in ovl_copyattr(), to pick up the new
size and times, so do it there. The one caller which hasn't changed
anything is ovl_inode_init(), give it __ovl_copyattr(). That leaves:
- copy up, which writes to the upper inode before the overlay inode knows
about it: ovl_inode_update()
- copy up of the data of a metacopy inode, which has its upper inode
already: ovl_copy_up_meta_inode_data()
- a new atime: ovl_update_time()
- a shared writable mapping, through which stores go straight to the upper
inode: ovl_mmap(). The next patch keeps such an inode dirty for as long
as the mapping can be written to.
The state of the upper inode isn't looked at. Its dirty flags are cleared
when its writeback starts, so a syncfs() during that writeback would miss
the inode.
Nothing is marked on a volatile overlay, which never syncs.
An earlier version marked every overlay inode with an upper inode dirty,
for good. That is simple, but the flusher then walks all of them every
time: 1.3 seconds for a million inodes, and with metacopy a container
which chowns its image has that many.
Based on work by Chengguang Xu.
Link: https://lore.kernel.org/r/20211122030038.1938875-1-cgxu519@xxxxxxxxxxxx
Suggested-by: Amir Goldstein <amir73il@xxxxxxxxx>
Suggested-by: Miklos Szeredi <miklos@xxxxxxxxxx>
Signed-off-by: Josef Bacik <josef@xxxxxxxxxxxxxx>
---
fs/overlayfs/copy_up.c | 2 ++
fs/overlayfs/file.c | 8 +++++++-
fs/overlayfs/inode.c | 4 +++-
fs/overlayfs/overlayfs.h | 14 ++++++++++++++
fs/overlayfs/util.c | 14 +++++++++++++-
5 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c
index e963701b4c87..12ad3ada5517 100644
--- a/fs/overlayfs/copy_up.c
+++ b/fs/overlayfs/copy_up.c
@@ -1122,6 +1122,8 @@ static int ovl_copy_up_meta_inode_data(struct ovl_copy_up_ctx *c)
ovl_clear_flag(OVL_HAS_DIGEST, d_inode(c->dentry));
ovl_clear_flag(OVL_VERIFIED_DIGEST, d_inode(c->dentry));
ovl_set_upperdata(d_inode(c->dentry));
+ /* The data is in the upper inode now, and nobody may ever write to it */
+ ovl_mark_inode_dirty(d_inode(c->dentry));
out_free:
kfree(capability);
out:
diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c
index 7433220d4ad6..cdc552c76878 100644
--- a/fs/overlayfs/file.c
+++ b/fs/overlayfs/file.c
@@ -473,7 +473,13 @@ static int ovl_mmap(struct file *file, struct vm_area_struct *vma)
.accessed = ovl_file_accessed,
};
- return backing_file_mmap(of->realfile, vma, &ctx);
+ int err;
+
+ err = backing_file_mmap(of->realfile, vma, &ctx);
+ /* Stores through the mapping go straight to the upper inode */
+ if (!err && vma_is_shared_maywrite(vma))
+ ovl_mark_inode_dirty(file_inode(file));
+ return err;
}
static long ovl_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c
index 63906b3dafcb..9a4bf7c2502b 100644
--- a/fs/overlayfs/inode.c
+++ b/fs/overlayfs/inode.c
@@ -587,6 +587,7 @@ int ovl_update_time(struct inode *inode, enum fs_update_time type,
touch_atime(&upperpath);
inode_set_atime_to_ts(inode,
inode_get_atime(d_inode(upperpath.dentry)));
+ ovl_mark_inode_dirty(inode);
}
}
return 0;
@@ -898,7 +899,8 @@ void ovl_inode_init(struct inode *inode, struct ovl_inode_params *oip,
oi->lowerdata_redirect = oip->lowerdata_redirect;
realinode = ovl_inode_real(inode);
- ovl_copyattr(inode);
+ /* Nothing was changed, the inode is only being looked up */
+ __ovl_copyattr(inode);
ovl_copyflags(realinode, inode);
ovl_map_ino(inode, ino, fsid);
}
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index 218f646b82b9..f5a17bc258b6 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -676,6 +676,19 @@ static inline bool ovl_tracks_dirty(struct ovl_fs *ofs)
(ovl_upper_mnt(ofs)->mnt_sb->s_iflags & SB_I_OVL_USE_SYNC_FS);
}
+/*
+ * The upper inode was changed through this overlay. Put the overlay inode on
+ * the dirty list of the overlay, for syncfs and unmount to find.
+ *
+ * Do not look at the state of the upper inode here. Its dirty flags are
+ * cleared when its writeback starts, not when it is done.
+ */
+static inline void ovl_mark_inode_dirty(struct inode *inode)
+{
+ if (ovl_tracks_dirty(OVL_FS(inode->i_sb)))
+ mark_inode_dirty(inode);
+}
+
static inline bool ovl_should_sync_metadata(struct ovl_fs *ofs)
{
return ofs->config.fsync_mode == OVL_FSYNC_STRICT;
@@ -873,6 +886,7 @@ bool ovl_lookup_trap_inode(struct super_block *sb, struct dentry *dir);
struct inode *ovl_get_trap_inode(struct super_block *sb, struct dentry *dir);
struct inode *ovl_get_inode(struct super_block *sb,
struct ovl_inode_params *oip);
+void __ovl_copyattr(struct inode *to);
void ovl_copyattr(struct inode *to);
/* vfs fileattr flags read from overlay.protattr xattr to ovl inode */
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index 521717209b2e..e7402459548e 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -604,6 +604,8 @@ void ovl_inode_update(struct inode *inode, struct dentry *upperdentry)
inode->i_private = upperinode;
__insert_inode_hash(inode, (unsigned long) upperinode);
}
+ /* The copy up has written to the upper inode */
+ ovl_mark_inode_dirty(inode);
}
static void ovl_dir_version_inc(struct dentry *dentry, bool impurity)
@@ -1492,7 +1494,7 @@ int ovl_sync_status(struct ovl_fs *ofs)
* map any lower or upper inode owned by id 1001 to id 1000. These mapping
* helpers are nops when the relevant layer isn't idmapped.
*/
-void ovl_copyattr(struct inode *inode)
+void __ovl_copyattr(struct inode *inode)
{
struct path realpath;
struct inode *realinode;
@@ -1516,3 +1518,13 @@ void ovl_copyattr(struct inode *inode)
i_size_write(inode, i_size_read(realinode));
spin_unlock(&inode->i_lock);
}
+
+/*
+ * The attributes are copied because the real inode was changed through the
+ * overlay, so this is also where the overlay inode becomes dirty.
+ */
+void ovl_copyattr(struct inode *inode)
+{
+ __ovl_copyattr(inode);
+ ovl_mark_inode_dirty(inode);
+}
--
2.55.0