* [PATCH 00/10] ovl: only sync what was written through the overlay
@ 2026-10-27 21:09 Josef Bacik
2026-10-27 21:09 ` [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS Josef Bacik
` (10 more replies)
0 siblings, 11 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik, Chengguang Xu
Hello,
This picks up Chengguang Xu's "implement containerized syncfs for
overlayfs", last posted as RFC V6 in November 2021 [1].
syncfs() on an overlay and unmounting one call sync_filesystem() on the
upper file system, so they write every dirty inode on it no matter who
dirtied it. We run a lot of containers on one upper file system, and
stopping one of them writes out the dirty data of all the others. That
can take a very long time, and s_umount of the upper file system is held
the whole time, so anything that wants it exclusively queues up and then
everybody else queues up behind that.
With this an overlay only writes the upper inodes that were changed
through it, and then has the upper file system commit its metadata.
The V6 discussion ended with a direction but no V7, so that's what I
tried to follow:
- V6 kept every overlay inode with an upper inode dirty for good. Jan
and Amir didn't like the cost of walking them. Now an overlay inode
is marked dirty when the upper inode is changed, mostly from
ovl_copyattr() as Amir and Miklos suggested.
- It never looks at the dirty flags of the upper inode, Miklos pointed
out that they're cleared when writeback starts.
- ->write_inode() goes through write_inode_now() on the upper inode,
and keeps the overlay inode dirty while the upper inode is open for
writing, which covers shared mappings.
- V6 marked the inodes DONTCACHE, which Jan was worried about. This
goes back to the v5 patch that keeps dirty overlay inodes cached.
One thing is new. V6 called wait_sb_inodes() on the upper file system to
catch writeback that was started but not finished, which waits for
everybody's writeback. Instead ->write_inode() keeps the overlay inode
dirty while the upper inode has pages tagged dirty or under writeback.
The tags don't have the problem the inode flags have, the writeback tag
is set before the dirty tag is cleared. So a clean overlay inode means
its data is on disk, and syncfs() only waits for its own inodes.
That takes care of the data. Metadata is harder, a lot of what is
changed on the upper file system has no overlay inode that could be on
the dirty list: the root of the overlay isn't hashed, and whiteouts,
index entries, the work directories and their parent have none. File
systems that log their metadata don't care, ->sync_fs() commits it. ext2
does, there a whiteout was gone after a crash. I first had overlayfs
write all of those itself and kept finding more of them, the last one
was the link count of the shared whiteout when a whiteout is removed.
So patch 1 adds a superblock flag, SB_I_OVL_USE_SYNC_FS, by which a file
system says that ->sync_fs() commits all of its metadata, and patches 2
to 4 set it for ext4 with a journal, xfs and btrfs. Without the flag on
the upper file system nothing changes, no inode is marked dirty and
syncfs() and unmount call sync_filesystem() like they always did.
Patches 5, 7, 8 and 10 are Chengguang's with changes noted in them,
patches 6 and 9 are new. I dropped Jan's Reviewed-by tags from V6 since
the patches changed.
lazytime on the upper file system needs its own handling. The flusher
leaves the timestamps in memory for up to 12 hours, so the overlay inode
can't be clean, but it can't stay dirty and pinned for that long either.
Patch 9 gives it I_DIRTY_TIME like the upper inode has, and forwards
->sync_lazytime() to the upper inode.
Things I know about:
- An overlay inode that loses its last reference while it is dirty is
kept until it has been written and then sits on the LRU, like with
any other file system. Clean and unlinked ones are still freed right
away.
- Every overlay on such an upper file system gets a bdi.
- I haven't measured syncfs() with a very large number of dirty inodes.
For testing I put several overlays on one upper file system and dirty
192M next to the overlay under test, through another overlay and
directly. Then syncfs() or unmount the one overlay, copy the block
device as it is at that moment, mount the copy, and check that
everything that was changed through the overlay is in it and that the
192M aren't. That passes with ext4, xfs and btrfs as the upper file
system. With KASAN and lockdep on, syncfs() goes from 239-284ms to
45-48ms and unmount from 126-234ms to 39-44ms. With ext2 and ext4
without a journal the 192M are written as before.
fstests with -overlay, the auto groups of generic and overlay, gives the
same results with and without the series on ext4 and on xfs.
[1] https://lore.kernel.org/r/20211122030038.1938875-1-cgxu519@mykernel.net
Thanks,
Josef
---
Chengguang Xu (4):
ovl: setup overlayfs' private bdi
ovl: implement overlayfs' own ->write_inode operation
ovl: cache dirty overlayfs' inode
ovl: implement containerized syncfs for overlayfs
Josef Bacik (6):
fs: add SB_I_OVL_USE_SYNC_FS
ext4: set SB_I_OVL_USE_SYNC_FS if there is a journal
xfs: set SB_I_OVL_USE_SYNC_FS
btrfs: set SB_I_OVL_USE_SYNC_FS
ovl: mark the overlay inode dirty on modification
ovl: handle lazytime on the upper filesystem
fs/btrfs/super.c | 2 +-
fs/ext4/super.c | 1 +
fs/overlayfs/copy_up.c | 2 +
fs/overlayfs/dir.c | 1 +
fs/overlayfs/file.c | 8 ++-
fs/overlayfs/inode.c | 21 +++++-
fs/overlayfs/overlayfs.h | 28 ++++++++
fs/overlayfs/super.c | 145 ++++++++++++++++++++++++++++++++++++++---
fs/overlayfs/util.c | 14 +++-
fs/xfs/xfs_super.c | 2 +-
include/linux/fs/super_types.h | 1 +
11 files changed, 211 insertions(+), 14 deletions(-)
---
base-commit: 2c8a968b488c134d05ccefff890270ac8ab899a8
change-id: 20261026-b4-ovl-containerized-sync-149bed1afe9a
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-28 15:21 ` Christoph Hellwig
2026-10-28 15:36 ` Amir Goldstein
2026-10-27 21:09 ` [PATCH 02/10] ext4: set SB_I_OVL_USE_SYNC_FS if there is a journal Josef Bacik
` (9 subsequent siblings)
10 siblings, 2 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
sync_filesystem() writes every dirty inode of a filesystem and then calls
->sync_fs(). The data needs that. Whether the metadata does depends on
the filesystem. ext2 copies an inode to its buffer in ->write_inode() and
nowhere else. A filesystem which logs its metadata has recorded the
change when it was made, and ->sync_fs() commits the log.
overlayfs wants to sync what was changed through one overlay instead of
the whole upper filesystem. It knows which inodes it wrote data to. It
has a much harder time with all the inodes which the upper filesystem
dirtied on its behalf, many of which it has no inode of its own for:
whiteouts, index entries, work directories, the parents of all of them.
With a filesystem which logs its metadata it doesn't have to know.
Add SB_I_OVL_USE_SYNC_FS, by which a filesystem says that ->sync_fs() with
wait set makes every change of metadata durable which was made before the
call, whether ->write_inode() was called for the inode or not. Timestamps
which lazytime holds back are not covered by that. As an upper filesystem
of overlayfs it then gets ->sync_fs() instead of sync_filesystem().
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
include/linux/fs/super_types.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
index 72f4288e16c5..370b0d27815b 100644
--- a/include/linux/fs/super_types.h
+++ b/include/linux/fs/super_types.h
@@ -359,5 +359,6 @@ struct super_block {
#define SB_I_NOIDMAP 0x00002000 /* No idmapped mounts on this superblock */
#define SB_I_ALLOW_HSM 0x00004000 /* Allow HSM events on this superblock */
#define SB_I_NO_DATA_INTEGRITY 0x00008000 /* fs cannot guarantee data persistence on sync */
+#define SB_I_OVL_USE_SYNC_FS 0x00010000 /* overlayfs: ->sync_fs() commits all metadata */
#endif /* _LINUX_FS_SUPER_TYPES_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 02/10] ext4: set SB_I_OVL_USE_SYNC_FS if there is a journal
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
2026-10-27 21:09 ` [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 03/10] xfs: set SB_I_OVL_USE_SYNC_FS Josef Bacik
` (8 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
With a journal every change of metadata is part of a transaction, and
ext4_sync_fs() commits the running one. ext4_write_inode() has nothing to
add to that, it only forces a commit as well.
Without a journal the flag stays clear.
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/ext4/super.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 3ace06b4f9e3..037e274dca8a 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -5542,6 +5542,7 @@ static int __ext4_fill_super(struct fs_context *fc, struct super_block *sb)
err = ext4_load_and_init_journal(sb, es, ctx);
if (err)
goto failed_mount3a;
+ sb->s_iflags |= SB_I_OVL_USE_SYNC_FS;
if (bdev_read_only(sb->s_bdev))
needs_recovery = 0;
} else if (test_opt(sb, NOLOAD) && !sb_rdonly(sb) &&
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 03/10] xfs: set SB_I_OVL_USE_SYNC_FS
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
2026-10-27 21:09 ` [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS Josef Bacik
2026-10-27 21:09 ` [PATCH 02/10] ext4: set SB_I_OVL_USE_SYNC_FS if there is a journal Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-28 15:19 ` Christoph Hellwig
2026-10-27 21:09 ` [PATCH 04/10] btrfs: " Josef Bacik
` (7 subsequent siblings)
10 siblings, 1 reply; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
All metadata is logged, xfs_fs_sync_fs() forces the log, and there is no
->write_inode().
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/xfs/xfs_super.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fs/xfs/xfs_super.c b/fs/xfs/xfs_super.c
index 5a06132aa384..2f09d48b7e6f 100644
--- a/fs/xfs/xfs_super.c
+++ b/fs/xfs/xfs_super.c
@@ -1924,7 +1924,7 @@ xfs_fs_fill_super(
sb->s_time_max = XFS_LEGACY_TIME_MAX;
}
trace_xfs_inode_timestamp_range(mp, sb->s_time_min, sb->s_time_max);
- sb->s_iflags |= SB_I_CGROUPWB | SB_I_ALLOW_HSM;
+ sb->s_iflags |= SB_I_CGROUPWB | SB_I_ALLOW_HSM | SB_I_OVL_USE_SYNC_FS;
set_posix_acl_flag(sb);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 04/10] btrfs: set SB_I_OVL_USE_SYNC_FS
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (2 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 03/10] xfs: set SB_I_OVL_USE_SYNC_FS Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 05/10] ovl: setup overlayfs' private bdi Josef Bacik
` (6 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
Metadata is changed in a transaction, and btrfs_sync_fs() waits for the
ordered extents and commits it. There is no ->write_inode().
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/btrfs/super.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c
index 7fabb775d4e3..cdecf90bdae0 100644
--- a/fs/btrfs/super.c
+++ b/fs/btrfs/super.c
@@ -981,7 +981,7 @@ static int btrfs_fill_super(struct super_block *sb,
#endif
sb->s_xattr = btrfs_xattr_handlers;
sb->s_time_gran = 1;
- sb->s_iflags |= SB_I_CGROUPWB | SB_I_ALLOW_HSM;
+ sb->s_iflags |= SB_I_CGROUPWB | SB_I_ALLOW_HSM | SB_I_OVL_USE_SYNC_FS;
ret = super_setup_bdi(sb);
if (ret) {
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 05/10] ovl: setup overlayfs' private bdi
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (3 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 04/10] btrfs: " Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 06/10] ovl: mark the overlay inode dirty on modification Josef Bacik
` (5 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Chengguang Xu, Josef Bacik
From: Chengguang Xu <charliecgxu@tencent.com>
Setup overlayfs' private bdi so that we can collect overlayfs' own dirty
inodes.
Signed-off-by: Chengguang Xu <charliecgxu@tencent.com>
[josef: only for an overlay which is going to have dirty inodes, rebased]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/overlayfs.h | 13 +++++++++++++
fs/overlayfs/super.c | 7 +++++++
2 files changed, 20 insertions(+)
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index 92da556598da..218f646b82b9 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -663,6 +663,19 @@ static inline bool ovl_should_sync(struct ovl_fs *ofs)
return ofs->config.fsync_mode != OVL_FSYNC_VOLATILE;
}
+/*
+ * Does this overlay keep track of the upper inodes which are changed through
+ * it, to sync only those? It knows where it wrote data. It does not know of
+ * every inode which the upper filesystem dirtied on its behalf, so that has to
+ * be a filesystem where ->sync_fs() takes care of all metadata. With any other
+ * one syncfs() and unmount sync the whole upper filesystem.
+ */
+static inline bool ovl_tracks_dirty(struct ovl_fs *ofs)
+{
+ return ovl_upper_mnt(ofs) && ovl_should_sync(ofs) &&
+ (ovl_upper_mnt(ofs)->mnt_sb->s_iflags & SB_I_OVL_USE_SYNC_FS);
+}
+
static inline bool ovl_should_sync_metadata(struct ovl_fs *ofs)
{
return ofs->config.fsync_mode == OVL_FSYNC_STRICT;
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 19b6d54c4500..d21e7cbb70c2 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -1455,6 +1455,13 @@ static int ovl_fill_super_creds(struct fs_context *fc, struct super_block *sb)
if (err)
return err;
+ /* Dirty overlay inodes need a writeback list to be on */
+ if (ovl_tracks_dirty(ofs)) {
+ err = super_setup_bdi(sb);
+ if (err)
+ return err;
+ }
+
upper_sb = ovl_upper_mnt(ofs)->mnt_sb;
if (!ovl_should_sync(ofs)) {
ofs->errseq = errseq_sample(&upper_sb->s_wb_err);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 06/10] ovl: mark the overlay inode dirty on modification
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (4 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 05/10] ovl: setup overlayfs' private bdi Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 07/10] ovl: implement overlayfs' own ->write_inode operation Josef Bacik
` (4 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
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@mykernel.net
Suggested-by: Amir Goldstein <amir73il@gmail.com>
Suggested-by: Miklos Szeredi <miklos@szeredi.hu>
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
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
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 07/10] ovl: implement overlayfs' own ->write_inode operation
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (5 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 06/10] ovl: mark the overlay inode dirty on modification Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 08/10] ovl: cache dirty overlayfs' inode Josef Bacik
` (3 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Chengguang Xu, Josef Bacik
From: Chengguang Xu <charliecgxu@tencent.com>
Sync dirty data and meta of upper inode in overlayfs' ->write_inode().
The overlay inode stays dirty for as long as there may be more to do:
while the upper inode is open for writing, because stores through a shared
mapping go unnoticed and the mapping holds the file open, and while the
upper inode has pages which are dirty or under writeback, because a pass
which doesn't wait has only started them. So by the time the overlay
inode is clean, the data which was written through it is on disk, and a
syncfs() which only looks at the dirty ones misses nothing.
Signed-off-by: Chengguang Xu <charliecgxu@tencent.com>
[josef: redirty only while the upper inode is unfinished instead of always,
no check for volatile, nothing is marked dirty there]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/super.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 52 insertions(+)
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index d21e7cbb70c2..0aec7059e9da 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -15,6 +15,8 @@
#include <linux/seq_file.h>
#include <linux/posix_acl_xattr.h>
#include <linux/exportfs.h>
+#include <linux/pagemap.h>
+#include <linux/writeback.h>
#include <linux/file.h>
#include <linux/fs_context.h>
#include <linux/fs_parser.h>
@@ -313,12 +315,62 @@ static int ovl_statfs(struct dentry *dentry, struct kstatfs *buf)
return err;
}
+/*
+ * Could there be anything left to write or to wait for on @upper?
+ *
+ * Whoever has it open for writing may have it mapped, and stores through a
+ * mapping go unnoticed. The mapping holds the file open.
+ *
+ * Writeback which was only started, by us or by the flusher of the upper
+ * filesystem, shows in the page cache tags until it is done. The dirty flags
+ * of the inode do not, they are cleared at the start.
+ *
+ * Metadata is not looked at. The upper filesystem logs it, see
+ * ovl_tracks_dirty(), and ovl_sync_fs() has the log committed.
+ *
+ * Nothing orders these reads. The one order which matters is to not see
+ * i_writecount at zero and miss the tag of a page which was dirtied through
+ * the mapping. The tag is set when the mapping goes away at the latest, the
+ * count drops when the file is released after that, with the locks of
+ * unmapping and of the final fput in between.
+ */
+static bool ovl_upper_unfinished(struct inode *upper)
+{
+ struct address_space *mapping = upper->i_mapping;
+
+ return inode_is_open_for_write(upper) ||
+ mapping_tagged(mapping, PAGECACHE_TAG_DIRTY) ||
+ mapping_tagged(mapping, PAGECACHE_TAG_WRITEBACK) ||
+ (inode_state_read_once(upper) & I_SYNC);
+}
+
+static int ovl_write_inode(struct inode *inode, struct writeback_control *wbc)
+{
+ struct inode *upper = ovl_inode_upper(inode);
+ int ret;
+
+ if (!upper)
+ return 0;
+
+ /*
+ * Go through writeback_single_inode(), I_SYNC is what serializes us
+ * with the writeback of the upper filesystem.
+ */
+ ret = write_inode_now(upper, wbc->sync_mode == WB_SYNC_ALL);
+
+ if (ovl_upper_unfinished(upper))
+ ovl_mark_inode_dirty(inode);
+
+ return ret;
+}
+
static const struct super_operations ovl_super_operations = {
.alloc_inode = ovl_alloc_inode,
.free_inode = ovl_free_inode,
.destroy_inode = ovl_destroy_inode,
.drop_inode = inode_just_drop,
.put_super = ovl_put_super,
+ .write_inode = ovl_write_inode,
.sync_fs = ovl_sync_fs,
.statfs = ovl_statfs,
.show_options = ovl_show_options,
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 08/10] ovl: cache dirty overlayfs' inode
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (6 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 07/10] ovl: implement overlayfs' own ->write_inode operation Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 09/10] ovl: handle lazytime on the upper filesystem Josef Bacik
` (2 subsequent siblings)
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
From: Chengguang Xu <cgxu519@mykernel.net>
An overlay inode is dropped with its last reference, dirty or not. Its
upper inode may well have dirty data at that point, and once the overlay
inode is gone nothing says that this data came from this overlay.
Only drop clean inodes. A dirty one stays in the cache until it has been
written back, and is then reclaimed from the lru list. One which was
unlinked is still dropped at once, so that its space is freed.
Signed-off-by: Chengguang Xu <cgxu519@mykernel.net>
[josef: an inode under writeback isn't clean either, no need to look for an
upper inode, rebased, new commit message]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/super.c | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 0aec7059e9da..954648d41700 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -364,11 +364,27 @@ static int ovl_write_inode(struct inode *inode, struct writeback_control *wbc)
return ret;
}
+/*
+ * In iput_final(), clean inode will drop directly and dirty inode will
+ * keep in the cache until write back to sync dirty data then add to lru
+ * list to wait reclaim.
+ *
+ * An inode under writeback has its dirty flags cleared, and only I_SYNC pins
+ * it. It is not clean: ovl_write_inode() may be about to mark it dirty again,
+ * which does nothing for an inode which is being freed.
+ */
+static int ovl_drop_inode(struct inode *inode)
+{
+ if (!(inode_state_read(inode) & (I_DIRTY_ALL | I_SYNC)))
+ return 1;
+ return inode_generic_drop(inode);
+}
+
static const struct super_operations ovl_super_operations = {
.alloc_inode = ovl_alloc_inode,
.free_inode = ovl_free_inode,
.destroy_inode = ovl_destroy_inode,
- .drop_inode = inode_just_drop,
+ .drop_inode = ovl_drop_inode,
.put_super = ovl_put_super,
.write_inode = ovl_write_inode,
.sync_fs = ovl_sync_fs,
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 09/10] ovl: handle lazytime on the upper filesystem
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (7 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 08/10] ovl: cache dirty overlayfs' inode Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-27 21:09 ` [PATCH 10/10] ovl: implement containerized syncfs for overlayfs Josef Bacik
2026-10-28 15:12 ` [PATCH 00/10] ovl: only sync what was written through the overlay Amir Goldstein
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Josef Bacik
With lazytime the timestamps of the upper inode stay in memory. A pass of
the flusher writes the data and leaves them alone, for up to 12 hours, so
the overlay inode can't be clean after it: a syncfs() which only looks at
dirty overlay inodes would miss the timestamps.
Keeping it dirty for that long isn't an option either. The flusher would
walk it every time, and a dirty inode can't be reclaimed. The upper inode
doesn't have that problem. It sits on a list of its own, and its
timestamps are written when its last reference goes away, which is what
happens under memory pressure. But the overlay inode holds a reference to
it.
Do the same one level up. When the timestamps are all that is left, give
the overlay inode I_DIRTY_TIME. That takes it off the list which the
flusher walks, while syncfs() still gets to it. Implement
->sync_lazytime() to pass on to the upper inode what the VFS tells the
overlay inode: on syncfs() and fsync(), when the 12 hours are over, and
when the last reference to the overlay inode goes away.
A change through the overlay to an inode which is parked like this also
writes the timestamps, because marking it dirty ends in ->sync_lazytime().
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/dir.c | 1 +
fs/overlayfs/inode.c | 17 +++++++++++++++++
fs/overlayfs/overlayfs.h | 1 +
fs/overlayfs/super.c | 24 ++++++++++++++++++++++++
4 files changed, 43 insertions(+)
diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c
index 1194ccf981c3..fc47beffa67e 100644
--- a/fs/overlayfs/dir.c
+++ b/fs/overlayfs/dir.c
@@ -1489,6 +1489,7 @@ const struct inode_operations ovl_dir_inode_operations = {
.get_acl = ovl_get_acl,
.set_acl = ovl_set_acl,
.update_time = ovl_update_time,
+ .sync_lazytime = ovl_sync_lazytime,
.fileattr_get = ovl_fileattr_get,
.fileattr_set = ovl_fileattr_set,
.tmpfile = ovl_tmpfile,
diff --git a/fs/overlayfs/inode.c b/fs/overlayfs/inode.c
index 9a4bf7c2502b..858802969017 100644
--- a/fs/overlayfs/inode.c
+++ b/fs/overlayfs/inode.c
@@ -743,6 +743,20 @@ int ovl_fileattr_get(struct dentry *dentry, struct file_kattr *fa)
return err;
}
+/*
+ * The overlay inode has I_DIRTY_TIME because the upper inode has, see
+ * ovl_park_inode(). Pass it on, and keep track of the upper inode until that
+ * has been written.
+ */
+void ovl_sync_lazytime(struct inode *inode)
+{
+ struct inode *upper = ovl_inode_upper(inode);
+
+ if (upper && (inode_state_read_once(upper) & I_DIRTY_TIME))
+ mark_inode_dirty_sync(upper);
+ ovl_mark_inode_dirty(inode);
+}
+
static const struct inode_operations ovl_file_inode_operations = {
.setattr = ovl_setattr,
.permission = ovl_permission,
@@ -752,6 +766,7 @@ static const struct inode_operations ovl_file_inode_operations = {
.get_acl = ovl_get_acl,
.set_acl = ovl_set_acl,
.update_time = ovl_update_time,
+ .sync_lazytime = ovl_sync_lazytime,
.fiemap = ovl_fiemap,
.fileattr_get = ovl_fileattr_get,
.fileattr_set = ovl_fileattr_set,
@@ -763,6 +778,7 @@ static const struct inode_operations ovl_symlink_inode_operations = {
.getattr = ovl_getattr,
.listxattr = ovl_listxattr,
.update_time = ovl_update_time,
+ .sync_lazytime = ovl_sync_lazytime,
};
static const struct inode_operations ovl_special_inode_operations = {
@@ -774,6 +790,7 @@ static const struct inode_operations ovl_special_inode_operations = {
.get_acl = ovl_get_acl,
.set_acl = ovl_set_acl,
.update_time = ovl_update_time,
+ .sync_lazytime = ovl_sync_lazytime,
};
static const struct address_space_operations ovl_aops = {
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index f5a17bc258b6..ad1084be6191 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -867,6 +867,7 @@ static inline struct posix_acl *ovl_get_acl_path(const struct path *path,
int ovl_update_time(struct inode *inode, enum fs_update_time type,
unsigned int flags);
+void ovl_sync_lazytime(struct inode *inode);
bool ovl_is_private_xattr(struct super_block *sb, const char *name);
struct ovl_inode_params {
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 954648d41700..631f68a97980 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -344,6 +344,28 @@ static bool ovl_upper_unfinished(struct inode *upper)
(inode_state_read_once(upper) & I_SYNC);
}
+/*
+ * All that is left of @inode are timestamps which lazytime holds back on the
+ * upper filesystem. A pass which does not wait leaves them alone, for up to 12
+ * hours. Do to the overlay inode what was done to the upper inode: with
+ * I_DIRTY_TIME it is off the list which the flusher walks, syncfs() still gets
+ * to it, and ovl_sync_lazytime() is told when the time has come.
+ *
+ * One such time is when the last reference goes away, which it never does for
+ * the upper inode as long as the overlay inode is there. It may have gone away
+ * already. iput() looks for I_DIRTY_TIME under i_lock before it drops the last
+ * reference, so either it sees the flag or the count is zero here.
+ */
+static void ovl_park_inode(struct inode *inode)
+{
+ if (!ovl_tracks_dirty(OVL_FS(inode->i_sb)))
+ return;
+
+ __mark_inode_dirty(inode, I_DIRTY_TIME);
+ if (!icount_read_once(inode))
+ mark_inode_dirty_sync(inode);
+}
+
static int ovl_write_inode(struct inode *inode, struct writeback_control *wbc)
{
struct inode *upper = ovl_inode_upper(inode);
@@ -360,6 +382,8 @@ static int ovl_write_inode(struct inode *inode, struct writeback_control *wbc)
if (ovl_upper_unfinished(upper))
ovl_mark_inode_dirty(inode);
+ else if (inode_state_read_once(upper) & I_DIRTY_TIME)
+ ovl_park_inode(inode);
return ret;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH 10/10] ovl: implement containerized syncfs for overlayfs
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (8 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 09/10] ovl: handle lazytime on the upper filesystem Josef Bacik
@ 2026-10-27 21:09 ` Josef Bacik
2026-10-28 15:12 ` [PATCH 00/10] ovl: only sync what was written through the overlay Amir Goldstein
10 siblings, 0 replies; 15+ messages in thread
From: Josef Bacik @ 2026-10-27 21:09 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Theodore Ts'o, Carlos Maiolino,
David Sterba, Chris Mason, Chengguang Xu, linux-unionfs,
linux-fsdevel, linux-ext4, linux-xfs, linux-btrfs, linux-kernel,
Chengguang Xu, Josef Bacik
From: Chengguang Xu <charliecgxu@tencent.com>
syncfs(2) on an overlay and unmounting one call sync_filesystem() on the
upper filesystem, which writes every dirty inode there is on it, whoever
dirtied it. When many containers share an upper filesystem, stopping one
of them writes out the dirty data of all the others. That can take a very
long time, and s_umount of the upper filesystem is held shared all the
while, so one remount or freeze of it which queues up behind that stops
everybody.
Now overlayfs can only sync own dirty inodes during syncfs, so remove
unnecessary sync_filesystem() on upper file system. What remains is to
have the upper filesystem commit its metadata, with ->sync_fs(), and to
write its block device as sync_filesystem() does.
That is for an upper filesystem with SB_I_OVL_USE_SYNC_FS. With any other
one nothing changes.
Signed-off-by: Chengguang Xu <charliecgxu@tencent.com>
[josef: no wait_sb_inodes() on the upper filesystem, which waits for
everybody's writeback: an overlay inode stays dirty until its own is done.
Only with SB_I_OVL_USE_SYNC_FS. Use sync_blockdev(). Return the error of
->sync_fs(). Nothing if the upper filesystem is read-only. More in the
commit message]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/super.c | 44 ++++++++++++++++++++++++++++++++++++--------
1 file changed, 36 insertions(+), 8 deletions(-)
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 631f68a97980..c570bf7103f8 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -15,6 +15,7 @@
#include <linux/seq_file.h>
#include <linux/posix_acl_xattr.h>
#include <linux/exportfs.h>
+#include <linux/blkdev.h>
#include <linux/pagemap.h>
#include <linux/writeback.h>
#include <linux/file.h>
@@ -269,18 +270,45 @@ static int ovl_sync_fs(struct super_block *sb, int wait)
/*
* Not called for sync(2) call or an emergency sync (SB_I_SKIP_SYNC).
* All the super blocks will be iterated, including upper_sb.
- *
- * If this is a syncfs(2) call, then we do need to call
- * sync_filesystem() on upper_sb, but enough if we do it when being
- * called with wait == 1.
*/
- if (!wait)
- return 0;
-
upper_sb = ovl_upper_mnt(ofs)->mnt_sb;
+ if (!ovl_tracks_dirty(ofs)) {
+ /*
+ * If this is a syncfs(2) call, then we do need to call
+ * sync_filesystem() on upper_sb, but enough if we do it when
+ * being called with wait == 1.
+ */
+ if (!wait)
+ return 0;
+
+ down_read(&upper_sb->s_umount);
+ ret = sync_filesystem(upper_sb);
+ up_read(&upper_sb->s_umount);
+
+ return ret;
+ }
+
+ /*
+ * For syncfs(2) and for unmount, sync_filesystem() has written the
+ * dirty inodes of this overlay by now, see ovl_write_inode(). What is
+ * left is for the upper filesystem to commit its metadata. The dirty
+ * inodes of everybody else on the upper filesystem are none of our
+ * business.
+ */
down_read(&upper_sb->s_umount);
- ret = sync_filesystem(upper_sb);
+ /* As in sync_filesystem(): ->sync_fs() doesn't expect to be called then */
+ ret = 0;
+ if (sb_rdonly(upper_sb))
+ goto out;
+
+ if (upper_sb->s_op->sync_fs)
+ ret = upper_sb->s_op->sync_fs(upper_sb, wait);
+ if (!ret && wait)
+ ret = sync_blockdev(upper_sb->s_bdev);
+ else if (!ret)
+ ret = sync_blockdev_nowait(upper_sb->s_bdev);
+out:
up_read(&upper_sb->s_umount);
return ret;
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH 00/10] ovl: only sync what was written through the overlay
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
` (9 preceding siblings ...)
2026-10-27 21:09 ` [PATCH 10/10] ovl: implement containerized syncfs for overlayfs Josef Bacik
@ 2026-10-28 15:12 ` Amir Goldstein
10 siblings, 0 replies; 15+ messages in thread
From: Amir Goldstein @ 2026-10-28 15:12 UTC (permalink / raw)
To: Josef Bacik
Cc: Miklos Szeredi, Christian Brauner, Alexander Viro, Jan Kara,
Theodore Ts'o, Carlos Maiolino, David Sterba, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-ext4,
linux-xfs, linux-btrfs, linux-kernel, Chengguang Xu
On Tue, Oct 27, 2026 at 11:09 PM Josef Bacik <josef@toxicpanda.com> wrote:
>
> Hello,
>
> This picks up Chengguang Xu's "implement containerized syncfs for
> overlayfs", last posted as RFC V6 in November 2021 [1].
>
Thanks for picking this up!
This looks like a very useful improvement.
> syncfs() on an overlay and unmounting one call sync_filesystem() on the
> upper file system, so they write every dirty inode on it no matter who
> dirtied it. We run a lot of containers on one upper file system, and
> stopping one of them writes out the dirty data of all the others. That
> can take a very long time, and s_umount of the upper file system is held
> the whole time, so anything that wants it exclusively queues up and then
> everybody else queues up behind that.
>
> With this an overlay only writes the upper inodes that were changed
> through it, and then has the upper file system commit its metadata.
>
> The V6 discussion ended with a direction but no V7, so that's what I
> tried to follow:
>
> - V6 kept every overlay inode with an upper inode dirty for good. Jan
> and Amir didn't like the cost of walking them. Now an overlay inode
> is marked dirty when the upper inode is changed, mostly from
> ovl_copyattr() as Amir and Miklos suggested.
> - It never looks at the dirty flags of the upper inode, Miklos pointed
> out that they're cleared when writeback starts.
> - ->write_inode() goes through write_inode_now() on the upper inode,
> and keeps the overlay inode dirty while the upper inode is open for
> writing, which covers shared mappings.
> - V6 marked the inodes DONTCACHE, which Jan was worried about. This
> goes back to the v5 patch that keeps dirty overlay inodes cached.
>
> One thing is new. V6 called wait_sb_inodes() on the upper file system to
> catch writeback that was started but not finished, which waits for
> everybody's writeback. Instead ->write_inode() keeps the overlay inode
> dirty while the upper inode has pages tagged dirty or under writeback.
> The tags don't have the problem the inode flags have, the writeback tag
> is set before the dirty tag is cleared. So a clean overlay inode means
> its data is on disk, and syncfs() only waits for its own inodes.
>
> That takes care of the data. Metadata is harder, a lot of what is
> changed on the upper file system has no overlay inode that could be on
> the dirty list: the root of the overlay isn't hashed, and whiteouts,
> index entries, the work directories and their parent have none. File
> systems that log their metadata don't care, ->sync_fs() commits it. ext2
> does, there a whiteout was gone after a crash. I first had overlayfs
> write all of those itself and kept finding more of them, the last one
> was the link count of the shared whiteout when a whiteout is removed.
>
> So patch 1 adds a superblock flag, SB_I_OVL_USE_SYNC_FS, by which a file
> system says that ->sync_fs() commits all of its metadata,
I have some reservations about the flag name (how could I not)
but mostly because I am considering if this flag could serve something else
as well (more on patch 1 review).
> and patches 2
> to 4 set it for ext4 with a journal, xfs and btrfs. Without the flag on
> the upper file system nothing changes, no inode is marked dirty and
> syncfs() and unmount call sync_filesystem() like they always did.
>
> Patches 5, 7, 8 and 10 are Chengguang's with changes noted in them,
> patches 6 and 9 are new. I dropped Jan's Reviewed-by tags from V6 since
> the patches changed.
>
> lazytime on the upper file system needs its own handling. The flusher
> leaves the timestamps in memory for up to 12 hours, so the overlay inode
> can't be clean, but it can't stay dirty and pinned for that long either.
> Patch 9 gives it I_DIRTY_TIME like the upper inode has, and forwards
> ->sync_lazytime() to the upper inode.
>
I suggest asking review from Jan again for those writeback and lazytime
bits. Would be hard for me to validate those.
Thanks,
Amir.
> Things I know about:
>
> - An overlay inode that loses its last reference while it is dirty is
> kept until it has been written and then sits on the LRU, like with
> any other file system. Clean and unlinked ones are still freed right
> away.
> - Every overlay on such an upper file system gets a bdi.
> - I haven't measured syncfs() with a very large number of dirty inodes.
>
> For testing I put several overlays on one upper file system and dirty
> 192M next to the overlay under test, through another overlay and
> directly. Then syncfs() or unmount the one overlay, copy the block
> device as it is at that moment, mount the copy, and check that
> everything that was changed through the overlay is in it and that the
> 192M aren't. That passes with ext4, xfs and btrfs as the upper file
> system. With KASAN and lockdep on, syncfs() goes from 239-284ms to
> 45-48ms and unmount from 126-234ms to 39-44ms. With ext2 and ext4
> without a journal the 192M are written as before.
>
> fstests with -overlay, the auto groups of generic and overlay, gives the
> same results with and without the series on ext4 and on xfs.
>
> [1] https://lore.kernel.org/r/20211122030038.1938875-1-cgxu519@mykernel.net
>
> Thanks,
>
> Josef
>
> ---
> Chengguang Xu (4):
> ovl: setup overlayfs' private bdi
> ovl: implement overlayfs' own ->write_inode operation
> ovl: cache dirty overlayfs' inode
> ovl: implement containerized syncfs for overlayfs
>
> Josef Bacik (6):
> fs: add SB_I_OVL_USE_SYNC_FS
> ext4: set SB_I_OVL_USE_SYNC_FS if there is a journal
> xfs: set SB_I_OVL_USE_SYNC_FS
> btrfs: set SB_I_OVL_USE_SYNC_FS
> ovl: mark the overlay inode dirty on modification
> ovl: handle lazytime on the upper filesystem
>
> fs/btrfs/super.c | 2 +-
> fs/ext4/super.c | 1 +
> fs/overlayfs/copy_up.c | 2 +
> fs/overlayfs/dir.c | 1 +
> fs/overlayfs/file.c | 8 ++-
> fs/overlayfs/inode.c | 21 +++++-
> fs/overlayfs/overlayfs.h | 28 ++++++++
> fs/overlayfs/super.c | 145 ++++++++++++++++++++++++++++++++++++++---
> fs/overlayfs/util.c | 14 +++-
> fs/xfs/xfs_super.c | 2 +-
> include/linux/fs/super_types.h | 1 +
> 11 files changed, 211 insertions(+), 14 deletions(-)
> ---
> base-commit: 2c8a968b488c134d05ccefff890270ac8ab899a8
> change-id: 20261026-b4-ovl-containerized-sync-149bed1afe9a
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 03/10] xfs: set SB_I_OVL_USE_SYNC_FS
2026-10-27 21:09 ` [PATCH 03/10] xfs: set SB_I_OVL_USE_SYNC_FS Josef Bacik
@ 2026-10-28 15:19 ` Christoph Hellwig
0 siblings, 0 replies; 15+ messages in thread
From: Christoph Hellwig @ 2026-10-28 15:19 UTC (permalink / raw)
To: Josef Bacik
Cc: Miklos Szeredi, Amir Goldstein, Christian Brauner, Alexander Viro,
Jan Kara, Theodore Ts'o, Carlos Maiolino, David Sterba,
Chris Mason, Chengguang Xu, linux-unionfs, linux-fsdevel,
linux-ext4, linux-xfs, linux-btrfs, linux-kernel
On Tue, Oct 27, 2026 at 09:09:29PM +0000, Josef Bacik wrote:
> All metadata is logged, xfs_fs_sync_fs() forces the log, and there is no
> ->write_inode().
>
> Signed-off-by: Josef Bacik <josef@toxicpanda.com>
> ---
> fs/xfs/xfs_super.c | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/fs/xfs/xfs_super.c b/fs/xfs/xfs_super.c
> index 5a06132aa384..2f09d48b7e6f 100644
> --- a/fs/xfs/xfs_super.c
> +++ b/fs/xfs/xfs_super.c
> @@ -1924,7 +1924,7 @@ xfs_fs_fill_super(
> sb->s_time_max = XFS_LEGACY_TIME_MAX;
> }
> trace_xfs_inode_timestamp_range(mp, sb->s_time_min, sb->s_time_max);
> - sb->s_iflags |= SB_I_CGROUPWB | SB_I_ALLOW_HSM;
> + sb->s_iflags |= SB_I_CGROUPWB | SB_I_ALLOW_HSM | SB_I_OVL_USE_SYNC_FS;
Please name flags ater their behavior and not the user.
Even with the commit log I have no idea what this indicates.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS
2026-10-27 21:09 ` [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS Josef Bacik
@ 2026-10-28 15:21 ` Christoph Hellwig
2026-10-28 15:36 ` Amir Goldstein
1 sibling, 0 replies; 15+ messages in thread
From: Christoph Hellwig @ 2026-10-28 15:21 UTC (permalink / raw)
To: Josef Bacik
Cc: Miklos Szeredi, Amir Goldstein, Christian Brauner, Alexander Viro,
Jan Kara, Theodore Ts'o, Carlos Maiolino, David Sterba,
Chris Mason, Chengguang Xu, linux-unionfs, linux-fsdevel,
linux-ext4, linux-xfs, linux-btrfs, linux-kernel
On Tue, Oct 27, 2026 at 09:09:27PM +0000, Josef Bacik wrote:
> sync_filesystem() writes every dirty inode of a filesystem and then calls
> ->sync_fs(). The data needs that.
That second sentence makes absolutely zero sence and barely passes the
grammer checker, what is that supposed to mean?
>Whether the metadata does depends on
> the filesystem.
The metadata does what? Since when did data and metadata become
subjects that act on their own?
> ext2 copies an inode to its buffer in ->write_inode() and
> nowhere else.
Seriously, please rewrite this commit log to make sense. It reads
like utter garabage without any meaning.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS
2026-10-27 21:09 ` [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS Josef Bacik
2026-10-28 15:21 ` Christoph Hellwig
@ 2026-10-28 15:36 ` Amir Goldstein
1 sibling, 0 replies; 15+ messages in thread
From: Amir Goldstein @ 2026-10-28 15:36 UTC (permalink / raw)
To: Josef Bacik
Cc: Miklos Szeredi, Christian Brauner, Alexander Viro, Jan Kara,
Theodore Ts'o, Carlos Maiolino, David Sterba, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-ext4,
linux-xfs, linux-btrfs, linux-kernel
On Tue, Oct 27, 2026 at 11:09 PM Josef Bacik <josef@toxicpanda.com> wrote:
>
> sync_filesystem() writes every dirty inode of a filesystem and then calls
> ->sync_fs(). The data needs that. Whether the metadata does depends on
> the filesystem. ext2 copies an inode to its buffer in ->write_inode() and
> nowhere else. A filesystem which logs its metadata has recorded the
> change when it was made, and ->sync_fs() commits the log.
>
> overlayfs wants to sync what was changed through one overlay instead of
> the whole upper filesystem. It knows which inodes it wrote data to. It
> has a much harder time with all the inodes which the upper filesystem
> dirtied on its behalf, many of which it has no inode of its own for:
> whiteouts, index entries, work directories, the parents of all of them.
> With a filesystem which logs its metadata it doesn't have to know.
>
> Add SB_I_OVL_USE_SYNC_FS, by which a filesystem says that ->sync_fs() with
> wait set makes every change of metadata durable which was made before the
> call, whether ->write_inode() was called for the inode or not. Timestamps
> which lazytime holds back are not covered by that. As an upper filesystem
> of overlayfs it then gets ->sync_fs() instead of sync_filesystem().
>
> Signed-off-by: Josef Bacik <josef@toxicpanda.com>
> ---
> include/linux/fs/super_types.h | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
> index 72f4288e16c5..370b0d27815b 100644
> --- a/include/linux/fs/super_types.h
> +++ b/include/linux/fs/super_types.h
> @@ -359,5 +359,6 @@ struct super_block {
> #define SB_I_NOIDMAP 0x00002000 /* No idmapped mounts on this superblock */
> #define SB_I_ALLOW_HSM 0x00004000 /* Allow HSM events on this superblock */
> #define SB_I_NO_DATA_INTEGRITY 0x00008000 /* fs cannot guarantee data persistence on sync */
> +#define SB_I_OVL_USE_SYNC_FS 0x00010000 /* overlayfs: ->sync_fs() commits all metadata */
>
I have some reservations about the flag name (how could I not).
Not easy, but can we try the flag to describe the contract?
I am interested to know if the same flag or maybe even a more
strict contract which is fulfilled by ext4/xfs/btrfs could be
used to help determine ovl_should_sync_metadata() -
In commit 7d6899fb69d25 ("ovl: fsync after metadata copy-up")
we added fsync on parent directories and metadata after copy up
to fix correctness on ubifs.
In commit 1f6ee9be92f8d ("ovl: make fsync after metadata copy-up
opt-in mount option") fsync=strict was created for opting-in to fsync
parent dirs on copy up, after a report on regression over ext4 when
due to the unconditional metadata fsync.
I am wondering whether fsync=auto could decide regarding fsync of
metadata on copy up depending on the flag, so metadata would be
fsynced on ubifs and not fsynced on ext4.
The problem is that your flag speaks of the metadata ordering contract
of sync_fs(sb) and my interest is the metadata ordering contract of
fsync(inode).
To be more specific, in this sequence:
mkdir t1
chmod 700 t1
mv t1 A
mkdir t2
chmod 700 t2
mv t2 A/B
touch t3
chmod 600 t3
sync -d t3
mv t3 A/B/foo
When A/B/foo is observed after a crash, in xfs I know that because
all metadata writes are serialized in the single transaction, the mode
of parents is correct.
Not sure about btrfs and ext4 fast commit.
If you think that the answer is that they provide the same guaranty
then if the flag could be named something like
SB_I_ORDERED_METADATA or something and
the contract is documented, then perhaps it also fits the
sync_fs case, because what you really want to know is that
sync_fs persistent the metadata that the inode depends on.
Am I talking nonsense?
Thanks,
Amir.
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-10-28 15:36 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-27 21:09 [PATCH 00/10] ovl: only sync what was written through the overlay Josef Bacik
2026-10-27 21:09 ` [PATCH 01/10] fs: add SB_I_OVL_USE_SYNC_FS Josef Bacik
2026-10-28 15:21 ` Christoph Hellwig
2026-10-28 15:36 ` Amir Goldstein
2026-10-27 21:09 ` [PATCH 02/10] ext4: set SB_I_OVL_USE_SYNC_FS if there is a journal Josef Bacik
2026-10-27 21:09 ` [PATCH 03/10] xfs: set SB_I_OVL_USE_SYNC_FS Josef Bacik
2026-10-28 15:19 ` Christoph Hellwig
2026-10-27 21:09 ` [PATCH 04/10] btrfs: " Josef Bacik
2026-10-27 21:09 ` [PATCH 05/10] ovl: setup overlayfs' private bdi Josef Bacik
2026-10-27 21:09 ` [PATCH 06/10] ovl: mark the overlay inode dirty on modification Josef Bacik
2026-10-27 21:09 ` [PATCH 07/10] ovl: implement overlayfs' own ->write_inode operation Josef Bacik
2026-10-27 21:09 ` [PATCH 08/10] ovl: cache dirty overlayfs' inode Josef Bacik
2026-10-27 21:09 ` [PATCH 09/10] ovl: handle lazytime on the upper filesystem Josef Bacik
2026-10-27 21:09 ` [PATCH 10/10] ovl: implement containerized syncfs for overlayfs Josef Bacik
2026-10-28 15:12 ` [PATCH 00/10] ovl: only sync what was written through the overlay Amir Goldstein
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox