* [PATCH v5 0/8] ovl: only sync what was written through the overlay
@ 2026-11-07 19:19 Josef Bacik
2026-11-07 19:19 ` [PATCH v5 1/8] ovl: expand fsync mount options Josef Bacik
` (7 more replies)
0 siblings, 8 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Josef Bacik, Chengguang Xu
v1: https://lore.kernel.org/all/20261027-b4-ovl-containerized-sync-v1-0-802e512379a6@toxicpanda.com/
v2: https://lore.kernel.org/all/20261029-b4-ovl-containerized-sync-v2-0-4e9f08e4a052@toxicpanda.com/
v3: https://lore.kernel.org/all/20261104-b4-ovl-containerized-sync-v3-0-8daa20144f53@toxicpanda.com/
v4: https://lore.kernel.org/all/20261106-b4-ovl-containerized-sync-v4-0-6ddde0b7dd8a@toxicpanda.com/
v4->v5:
- Added sync_fs_and_bdev() to fs/sync.c for the ->sync_fs() and block
device part of sync_filesystem(), so ovl_sync_fs() doesn't open code
it, patch 2. (Christian)
- ovl_instantiate() marks a new inode and a new hard link dirty.
(Christian, Sashiko)
- Copy up marks the parent as well, and the inode also when it only adds
a link to an indexed upper inode. (Sashiko)
- Dropped "ovl: make the root inode appear hashed". A fake hash stays in
the slab object when the inode is freed, and the next overlay inode in
it looks hashed. The root isn't tracked at all now, ovl_sync_fs()
writes its upper inode every time. (Sashiko)
- lazytime got a lot simpler. The overlay inode doesn't get I_DIRTY_TIME
any more, deferred timestamps are handed to the upper file system when
->write_inode() finds them. That takes care of the I/O from dentry
reclaim, so this doesn't need anything from Jan's deferred inode
reclaim series. (Sashiko, Christian)
- ovl_sync_upper_inode() could lose an error.
- Left alone: fsync=auto still goes by RENAME_WHITEOUT, no flag. What
that leaves uncovered is under "Things I know about". (Christian)
- Left alone: if somebody else starts the writeback of an upper inode
and it fails, syncfs() on the overlay doesn't report it. It never has.
(Sashiko)
v3->v4:
- Patch 1 is v2 of Amir's patch. It has ovl_tracks_dirty() and picks the
mode for tmpfs at mount time, so I dropped my version from patch 2.
fsync=auto stays auto if it doesn't become relaxed. (Amir)
v2->v3:
- Patch 1 is Amir's, it adds fsync=relaxed. Only an overlay with that
syncs just what was written through it, everybody else syncs the whole
upper file system as before. fsync=auto, the default, picks relaxed if
the upper file system supports RENAME_WHITEOUT. (Amir)
- Dropped the fake inodes and the calls in the ovl_do_*() helpers,
patches 7 to 9 of v2. Upper inodes without an overlay inode are left
to ->sync_fs() and the block device, like in v1. (Amir)
v1->v2:
- Dropped the superblock flag and the ext4, xfs and btrfs patches. This
is all in overlayfs now and works the same on any upper file system.
(Christoph, Amir)
- Upper inodes without an overlay inode get a fake one that goes on the
dirty list, patches 7 to 9.
- The periodic flusher pass doesn't write the upper inode any more, and
syncfs() doesn't make ext4 commit per inode.
- Record write errors in s_wb_err of the overlay.
- Make the root inode appear hashed so it can be on the dirty list,
don't mark unhashed inodes dirty, read i_writecount with acquire.
(Sashiko)
- Left alone: dirtied_time_when isn't set for an overlay inode that gets
I_DIRTY_TIME from ->write_inode(). It only matters for the 12 hour
expiry, where the upper file system writes the timestamps on its own.
(Sashiko)
- Rewrote my commit messages. (Christoph)
What it does now:
sync_filesystem() writes every inode on a dirty list, calls ->sync_fs()
and writes the block device. Only the first step is expensive, because
of other people's data. With fsync=relaxed overlayfs writes the upper
inodes whose overlay inode was marked dirty and does the other two steps
as they are.
Whiteouts and the work directories have no overlay inode, so nobody
writes those inodes. That's fine if ->sync_fs() or the block device gets
all metadata to disk, which is what relaxed asks of the upper file
system.
Things I know about:
- ext4 without a journal supports RENAME_WHITEOUT, so it gets relaxed.
It's fine there, its metadata is in the buffers of the block device,
which we write. I tested it like the others.
- f2fs, ubifs and fuse get relaxed as well and I didn't test them. f2fs
writes its dirty inodes in the checkpoint. ubifs_sync_fs() doesn't,
but the only thing overlayfs leaves dirty there on an inode it
doesn't track is the chmod of the work directory to 0 at mount time,
right after creating it with mode 0. fsync=on is there for anybody
who needs more.
- A default mount that gets relaxed shows that in /proc/mounts.
- lazytime on the upper file system doesn't do anything for inodes that
are modified or read through a relaxed overlay, their timestamps are
written with the next pass of the flusher.
- While a file is open for writing the flusher of the overlay looks at
its inode up to every 5 seconds. That doesn't do any I/O.
- With nobody else's data to skip syncfs() gets a bit slower. Over 30
boots per kernel, after creating 2000 files and a chown -R of another
2000 it goes from 51ms to 57ms on ext4 and takes 11ms longer on
btrfs. After 2000 mkdirs it goes from 45ms to 48ms on xfs and from
49ms to 54ms on btrfs, on ext4 I can't tell 235ms and 238ms apart.
Nothing else I timed moves.
The testing is the same as for v1, now also on ext4 without a journal.
syncfs() goes from 222-268ms to 41-50ms and unmount from 122-236ms to
36-45ms on those four. ext2 with [2], which is in Jan's tree now,
doesn't get relaxed and does exactly what it did before. fstests with
-overlay gives the same results with and without the series on ext4 and
on xfs, apart from overlay/019 on ext4 and generic/670, 733 and 751 on
xfs, which are flaky either way. There's a new fstest for this [3], it
fails if I break the tracking.
[2] https://lore.kernel.org/r/20261027-b4-ext2-dirsync-v1-1-3dcd012d518d@toxicpanda.com
[3] https://lore.kernel.org/r/773faf2a275e0a51bd38cf68f492856c.josef@toxicpanda.com
--- Original email (v1) ---
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
---
Changes in v5:
- Link to v4: https://patch.msgid.link/20261106-b4-ovl-containerized-sync-v4-0-6ddde0b7dd8a@toxicpanda.com
Changes in v4:
- Link to v3: https://patch.msgid.link/20261104-b4-ovl-containerized-sync-v3-0-8daa20144f53@toxicpanda.com
Changes in v3:
- Link to v2: https://patch.msgid.link/20261029-b4-ovl-containerized-sync-v2-0-4e9f08e4a052@toxicpanda.com
Changes in v2:
- Link to v1: https://patch.msgid.link/20261027-b4-ovl-containerized-sync-v1-0-802e512379a6@toxicpanda.com
---
Amir Goldstein (1):
ovl: expand fsync mount options
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 (3):
fs: add sync_fs_and_bdev()
ovl: mark the overlay inode dirty on modification
ovl: write the timestamps that lazytime defers
Documentation/filesystems/overlayfs.rst | 36 ++++++-
fs/overlayfs/Kconfig | 21 ++++
fs/overlayfs/copy_up.c | 9 ++
fs/overlayfs/dir.c | 3 +
fs/overlayfs/file.c | 7 +-
fs/overlayfs/inode.c | 4 +-
fs/overlayfs/overlayfs.h | 58 ++++++++++-
fs/overlayfs/params.c | 32 ++++---
fs/overlayfs/params.h | 2 +
fs/overlayfs/readdir.c | 1 +
fs/overlayfs/super.c | 165 +++++++++++++++++++++++++++++---
fs/overlayfs/util.c | 93 +++++++++++++++++-
fs/sync.c | 52 +++++++---
include/linux/fs.h | 1 +
14 files changed, 434 insertions(+), 50 deletions(-)
---
base-commit: 2c8a968b488c134d05ccefff890270ac8ab899a8
change-id: 20261026-b4-ovl-containerized-sync-149bed1afe9a
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 1/8] ovl: expand fsync mount options
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 2/8] fs: add sync_fs_and_bdev() Josef Bacik
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Josef Bacik
From: Amir Goldstein <amir73il@gmail.com>
The fsync mount option currently accepts three values:
"volatile", "strict" and "auto".
All other "enum" mount options have "on" and "off" values and this is
used by the generic _check_overlay_feature helper in fstests to probe
for support for enabling overlayfs features.
This commit adds support for values "on", "off" and "relaxed" and makes
the value of "auto" depend on a kconfig/module option fsync_auto.
The value of "off" behaves almost the same as "volatile", not
persisting changes to upper layer, but it does not create the directory
"$workdir/work/incompat/volatile", which prevents a mount cycle of a
volatile overlayfs. Some users requested to opt-out of this guard rail.
With upperdir on tmpfs, overlayfs now falls back to "fsync=off", because
the other fsync modes are futile on tmpfs.
The new value "relaxed" is a preparation for not syncing the entire
upper fs on syncfs(2) and unmount and instead tracking and syncing only
the upper inodes modified by this overlayfs.
The value of "on" is the traditional overlayfs behavior and it behaves
the same as the current value "auto", releasing the value of "auto" to
differ by the kconfig/module option fsync_auto.
By default, with kconfig/module option fsync_auto=Y, the meaning of the
value "auto" is that filesystems that support RENAME_WHITEOUT default to
"relaxed" and filesystems that do not support RENAME_WHITEOUT default to
the traditional "fsync=on".
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
Documentation/filesystems/overlayfs.rst | 36 ++++++++++++++++++++++++++++-----
fs/overlayfs/Kconfig | 21 +++++++++++++++++++
fs/overlayfs/overlayfs.h | 24 +++++++++++++++++++---
fs/overlayfs/params.c | 18 +++++++++++++----
fs/overlayfs/params.h | 2 ++
fs/overlayfs/super.c | 22 ++++++++++++++++++--
6 files changed, 109 insertions(+), 14 deletions(-)
diff --git a/Documentation/filesystems/overlayfs.rst b/Documentation/filesystems/overlayfs.rst
index 16c35b491dad..1a59500834a1 100644
--- a/Documentation/filesystems/overlayfs.rst
+++ b/Documentation/filesystems/overlayfs.rst
@@ -826,27 +826,53 @@ The fsync during copy up only guarantees that if a copy up is observed
after a crash, the observed data is not zeroes or intermediate values
from the copy up staging area.
+Traditionally, syncfs(2) and unmount of an overlay sync the entire upper
+filesystem, including the dirty data of other overlays that share it.
+Overlayfs can be told to only write back the files that were modified
+through the overlay, and then ask the upper filesystem to commit its
+metadata.
+
On traditional local filesystems with a single journal (e.g. ext4, xfs),
fsync on a file also persists the parent directory changes, because they
are usually modified in the same transaction, so metadata durability during
-data copy up effectively comes for free. Overlayfs further limits risk by
-disallowing network filesystems as upper layer.
+data copy up and the persistence of other upper changes such as whiteouts
+effectively come for free. Other filesystems (e.g. ubifs) require an explicit
+fsync(2) on the copied up files and directories to persist their metadata.
Overlayfs can be tuned to prefer performance or durability when storing
to the underlying upper layer. This is controlled by the "fsync" mount
option, which supports these values:
-- "auto": (default)
+- "volatile": [*]
+ Prefer performance over durability (see `Volatile mount`_)
+- "off":
+ Same as "volatile", but does not prevent mounting the overlay again
+ after unmount (see `Volatile mount`_)
+- "relaxed":
Call fsync(2) on upper file before completion of data copy up.
No explicit fsync(2) on directory or metadata-only copy up.
+ syncfs(2) and unmount only write back the files that were modified
+ through the overlay.
+- "auto": (default)
+ "relaxed" if the upper filesystem supports RENAME_WHITEOUT,
+ otherwise "on".
+- "on":
+ syncfs(2) syncs the entire upper filesystem.
+ Copy up behaves like "relaxed".
- "strict":
+ syncfs(2) syncs the entire upper filesystem.
Call fsync(2) on upper file and directories before completion of any
copy up.
-- "volatile": [*]
- Prefer performance over durability (see `Volatile mount`_)
[*] The mount option "volatile" is an alias to "fsync=volatile".
+The default is "fsync=on" instead of "fsync=auto" with the module option
+"fsync_auto=N" or with the kernel config option
+CONFIG_OVERLAY_FS_FSYNC_AUTO=n.
+
+If the upper filesystem has no writeback (e.g. tmpfs), a non-default mode
+falls back to "fsync=off".
+
Volatile mount
--------------
diff --git a/fs/overlayfs/Kconfig b/fs/overlayfs/Kconfig
index 2ac67e04a6fb..f1b2f3784c76 100644
--- a/fs/overlayfs/Kconfig
+++ b/fs/overlayfs/Kconfig
@@ -108,6 +108,27 @@ config OVERLAY_FS_XINO_AUTO
If unsure, say N.
+config OVERLAY_FS_FSYNC_AUTO
+ bool "Overlayfs: auto select the fsync mode by default"
+ default y
+ depends on OVERLAY_FS
+ help
+ If this config option is enabled then overlay filesystems will use
+ "fsync=auto" by default, which selects "fsync=relaxed" if the upper
+ filesystem supports RENAME_WHITEOUT and "fsync=on" otherwise.
+ With "fsync=relaxed", syncfs(2) and unmount of an overlay only write
+ back the files that were modified through the overlay, instead of
+ syncing the entire upper filesystem.
+
+ If this config option is disabled then overlay filesystems will use
+ "fsync=on" by default. It is still possible to change the default
+ with the "fsync_auto" module option or on a filesystem instance
+ basis with the "fsync" mount option.
+
+ For more information, see Documentation/filesystems/overlayfs.rst
+
+ If unsure, say Y.
+
config OVERLAY_FS_METACOPY
bool "Overlayfs: turn on metadata only copy up feature by default"
depends on OVERLAY_FS
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index 92da556598da..5b1a097ce543 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -102,8 +102,11 @@ enum {
};
enum {
+ OVL_FSYNC_OFF,
OVL_FSYNC_VOLATILE,
+ OVL_FSYNC_RELAXED,
OVL_FSYNC_AUTO,
+ OVL_FSYNC_ON,
OVL_FSYNC_STRICT,
};
@@ -660,7 +663,8 @@ static inline bool ovl_xino_warn(struct ovl_fs *ofs)
static inline bool ovl_should_sync(struct ovl_fs *ofs)
{
- return ofs->config.fsync_mode != OVL_FSYNC_VOLATILE;
+ return ofs->config.fsync_mode != OVL_FSYNC_OFF &&
+ ofs->config.fsync_mode != OVL_FSYNC_VOLATILE;
}
static inline bool ovl_should_sync_metadata(struct ovl_fs *ofs)
@@ -668,9 +672,23 @@ static inline bool ovl_should_sync_metadata(struct ovl_fs *ofs)
return ofs->config.fsync_mode == OVL_FSYNC_STRICT;
}
-static inline bool ovl_is_volatile(struct ovl_config *config)
+/*
+ * With fsync=relaxed, track the files that were modified through the overlay,
+ * so that syncfs(2) and unmount only write back those.
+ */
+static inline bool ovl_tracks_dirty(struct ovl_fs *ofs)
+{
+ return ofs->config.fsync_mode == OVL_FSYNC_RELAXED;
+}
+
+/*
+ * With fsync=relaxed, syncfs(2) and unmount only write back the files that were
+ * modified through the overlay. Otherwise they sync the entire upper
+ * filesystem.
+ */
+static inline bool ovl_should_sync_upper_fs(struct ovl_fs *ofs)
{
- return config->fsync_mode == OVL_FSYNC_VOLATILE;
+ return ovl_should_sync(ofs) && !ovl_tracks_dirty(ofs);
}
/*
diff --git a/fs/overlayfs/params.c b/fs/overlayfs/params.c
index 5a15e5478a6b..549fc1e83ac9 100644
--- a/fs/overlayfs/params.c
+++ b/fs/overlayfs/params.c
@@ -28,6 +28,11 @@ module_param_named(xino_auto, ovl_xino_auto_def, bool, 0644);
MODULE_PARM_DESC(xino_auto,
"Auto enable xino feature");
+static bool ovl_fsync_auto_def = IS_ENABLED(CONFIG_OVERLAY_FS_FSYNC_AUTO);
+module_param_named(fsync_auto, ovl_fsync_auto_def, bool, 0644);
+MODULE_PARM_DESC(fsync_auto,
+ "Default to fsync=auto instead of fsync=on");
+
static bool ovl_index_def = IS_ENABLED(CONFIG_OVERLAY_FS_INDEX);
module_param_named(index, ovl_index_def, bool, 0644);
MODULE_PARM_DESC(index,
@@ -142,8 +147,11 @@ static int ovl_verity_mode_def(void)
}
static const struct constant_table ovl_parameter_fsync[] = {
+ { "off", OVL_FSYNC_OFF },
{ "volatile", OVL_FSYNC_VOLATILE },
+ { "relaxed", OVL_FSYNC_RELAXED },
{ "auto", OVL_FSYNC_AUTO },
+ { "on", OVL_FSYNC_ON },
{ "strict", OVL_FSYNC_STRICT },
{}
};
@@ -153,9 +161,9 @@ static const char *ovl_fsync_mode(struct ovl_config *config)
return ovl_parameter_fsync[config->fsync_mode].name;
}
-static int ovl_fsync_mode_def(void)
+int ovl_fsync_mode_def(void)
{
- return OVL_FSYNC_AUTO;
+ return ovl_fsync_auto_def ? OVL_FSYNC_AUTO : OVL_FSYNC_ON;
}
const struct fs_parameter_spec ovl_parameter_spec[] = {
@@ -689,9 +697,11 @@ static int ovl_parse_param(struct fs_context *fc, struct fs_parameter *param)
break;
case Opt_fsync:
config->fsync_mode = result.uint_32;
+ ctx->set.fsync = true;
break;
case Opt_volatile:
config->fsync_mode = OVL_FSYNC_VOLATILE;
+ ctx->set.fsync = true;
break;
case Opt_userxattr:
config->userxattr = true;
@@ -896,8 +906,8 @@ int ovl_fs_params_verify(const struct ovl_fs_context *ctx,
config->index = false;
}
- if (!config->upperdir && ovl_is_volatile(config)) {
- pr_info("option \"volatile\" is meaningless in a non-upper mount, ignoring it.\n");
+ if (!config->upperdir && set.fsync) {
+ pr_info("options \"fsync\" and \"volatile\" are meaningless in a non-upper mount, ignoring them.\n");
config->fsync_mode = ovl_fsync_mode_def();
}
diff --git a/fs/overlayfs/params.h b/fs/overlayfs/params.h
index ffd53cdd8482..5f3572b270f3 100644
--- a/fs/overlayfs/params.h
+++ b/fs/overlayfs/params.h
@@ -15,6 +15,7 @@ struct ovl_opt_set {
bool redirect;
bool nfs_export;
bool index;
+ bool fsync;
};
#define OVL_MAX_STACK 500
@@ -42,3 +43,4 @@ int ovl_fs_params_verify(const struct ovl_fs_context *ctx,
struct ovl_config *config);
int ovl_show_options(struct seq_file *m, struct dentry *dentry);
const char *ovl_xino_mode(struct ovl_config *config);
+int ovl_fsync_mode_def(void);
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 19b6d54c4500..81da70fd53a6 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/backing-dev.h>
#include <linux/file.h>
#include <linux/fs_context.h>
#include <linux/fs_parser.h>
@@ -737,6 +738,11 @@ static int ovl_make_workdir(struct super_block *sb, struct ovl_fs *ofs,
if (!rename_whiteout)
pr_warn("upper fs does not support RENAME_WHITEOUT.\n");
+ /* fsync=auto that is not resolved to fsync=relaxed means fsync=on */
+ if (ofs->config.fsync_mode == OVL_FSYNC_AUTO && rename_whiteout &&
+ mnt->mnt_sb->s_bdi != &noop_backing_dev_info)
+ ofs->config.fsync_mode = OVL_FSYNC_RELAXED;
+
/*
* Check if upper/work fs supports (trusted|user).overlay.* xattr
*/
@@ -789,9 +795,9 @@ static int ovl_make_workdir(struct super_block *sb, struct ovl_fs *ofs,
/*
* For volatile mount, create a incompat/volatile/dirty file to keep
- * track of it.
+ * track of it. fsync=off allows mounting the overlay again.
*/
- if (ovl_is_volatile(&ofs->config)) {
+ if (ofs->config.fsync_mode == OVL_FSYNC_VOLATILE) {
err = ovl_create_volatile_dirty(ofs);
if (err < 0) {
pr_err("Failed to create volatile/dirty file.\n");
@@ -1456,6 +1462,18 @@ static int ovl_fill_super_creds(struct fs_context *fc, struct super_block *sb)
return err;
upper_sb = ovl_upper_mnt(ofs)->mnt_sb;
+ /*
+ * An upper fs without writeback has nothing to sync and nothing
+ * to lose, so every fsync mode behaves like fsync=off. Keep the
+ * default mode, which is not shown in mountinfo.
+ */
+ if (upper_sb->s_bdi == &noop_backing_dev_info &&
+ ofs->config.fsync_mode != ovl_fsync_mode_def() &&
+ ofs->config.fsync_mode != OVL_FSYNC_OFF) {
+ pr_warn("upper fs does not support writeback, falling back to fsync=off.\n");
+ ofs->config.fsync_mode = OVL_FSYNC_OFF;
+ }
+
if (!ovl_should_sync(ofs)) {
ofs->errseq = errseq_sample(&upper_sb->s_wb_err);
if (errseq_check(&upper_sb->s_wb_err, ofs->errseq)) {
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 2/8] fs: add sync_fs_and_bdev()
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
2026-11-07 19:19 ` [PATCH v5 1/8] ovl: expand fsync mount options Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 3/8] ovl: setup overlayfs' private bdi Josef Bacik
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Josef Bacik
sync_filesystem() writes the inodes, calls ->sync_fs() and writes the block
device, first without waiting and then with.
overlayfs is about to write the inodes of its upper filesystem by itself,
only those that were modified through the overlay, and needs the other two
steps as they are. Move them to a helper so that overlayfs does not have
to know what they are.
sync_filesystem() calls the inner helper, so it still tests for a read-only
filesystem and for s_umount once. The exported function does both tests
for its callers.
No functional change intended.
Suggested-by: Christian Brauner <brauner@kernel.org>
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/sync.c | 52 +++++++++++++++++++++++++++++++++++++++-------------
include/linux/fs.h | 1 +
2 files changed, 40 insertions(+), 13 deletions(-)
diff --git a/fs/sync.c b/fs/sync.c
index 0681bfcb2da6..c5e49914918a 100644
--- a/fs/sync.c
+++ b/fs/sync.c
@@ -22,6 +22,42 @@
#define VALID_FLAGS (SYNC_FILE_RANGE_WAIT_BEFORE|SYNC_FILE_RANGE_WRITE| \
SYNC_FILE_RANGE_WAIT_AFTER)
+static int __sync_fs_and_bdev(struct super_block *sb, bool wait)
+{
+ int ret;
+
+ if (sb->s_op->sync_fs) {
+ ret = sb->s_op->sync_fs(sb, wait);
+ if (ret)
+ return ret;
+ }
+ if (!wait)
+ return sync_blockdev_nowait(sb->s_bdev);
+ return sync_blockdev(sb->s_bdev);
+}
+
+/**
+ * sync_fs_and_bdev - call ->sync_fs() and write the block device
+ * @sb: the superblock, the caller holds s_umount
+ * @wait: wait for the I/O to complete
+ *
+ * These are the steps of sync_filesystem() that follow the writing of the
+ * inodes. This is for a caller that has already written the inodes it cares
+ * about. Like sync_filesystem() it does nothing on a read-only filesystem.
+ *
+ * Return: 0 on success, a negative error code otherwise.
+ */
+int sync_fs_and_bdev(struct super_block *sb, bool wait)
+{
+ WARN_ON(!rwsem_is_locked(&sb->s_umount));
+
+ if (sb_rdonly(sb))
+ return 0;
+
+ return __sync_fs_and_bdev(sb, wait);
+}
+EXPORT_SYMBOL_GPL(sync_fs_and_bdev);
+
/*
* Write out and wait upon all dirty data associated with this
* superblock. Filesystem data as well as the underlying block
@@ -29,7 +65,7 @@
*/
int sync_filesystem(struct super_block *sb)
{
- int ret = 0;
+ int ret;
/*
* We need to be protected against the filesystem going from
@@ -52,22 +88,12 @@ int sync_filesystem(struct super_block *sb)
* at a time.
*/
writeback_inodes_sb(sb, WB_REASON_SYNC);
- if (sb->s_op->sync_fs) {
- ret = sb->s_op->sync_fs(sb, 0);
- if (ret)
- return ret;
- }
- ret = sync_blockdev_nowait(sb->s_bdev);
+ ret = __sync_fs_and_bdev(sb, false);
if (ret)
return ret;
sync_inodes_sb(sb);
- if (sb->s_op->sync_fs) {
- ret = sb->s_op->sync_fs(sb, 1);
- if (ret)
- return ret;
- }
- return sync_blockdev(sb->s_bdev);
+ return __sync_fs_and_bdev(sb, true);
}
EXPORT_SYMBOL(sync_filesystem);
diff --git a/include/linux/fs.h b/include/linux/fs.h
index b5485f2c1f2b..094a4be6828c 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2617,6 +2617,7 @@ extern void __init vfs_caches_init(void);
void emergency_thaw_all(void);
extern int sync_filesystem(struct super_block *);
+int sync_fs_and_bdev(struct super_block *sb, bool wait);
extern const struct file_operations def_blk_fops;
extern const struct file_operations def_chr_fops;
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 3/8] ovl: setup overlayfs' private bdi
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
2026-11-07 19:19 ` [PATCH v5 1/8] ovl: expand fsync mount options Josef Bacik
2026-11-07 19:19 ` [PATCH v5 2/8] fs: add sync_fs_and_bdev() Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 4/8] ovl: mark the overlay inode dirty on modification Josef Bacik
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, 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 that tracks dirty inodes]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/super.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 81da70fd53a6..22a8d4eb3b7c 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -1490,6 +1490,16 @@ static int ovl_fill_super_creds(struct fs_context *fc, struct super_block *sb)
if (!ofs->workdir)
sb->s_flags |= SB_RDONLY;
+ /*
+ * Dirty overlay inodes go on the lists of this bdi. fsync_mode
+ * is final once ovl_get_workdir() has returned.
+ */
+ if (ovl_tracks_dirty(ofs)) {
+ err = super_setup_bdi(sb);
+ if (err)
+ return err;
+ }
+
sb->s_stack_depth = upper_sb->s_stack_depth;
sb->s_time_gran = upper_sb->s_time_gran;
}
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 4/8] ovl: mark the overlay inode dirty on modification
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
` (2 preceding siblings ...)
2026-11-07 19:19 ` [PATCH v5 3/8] ovl: setup overlayfs' private bdi Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 5/8] ovl: implement overlayfs' own ->write_inode operation Josef Bacik
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Josef Bacik
To sync only what was written through an overlay, we have to know which
upper inodes were modified through it. Mark the overlay inode dirty when
its upper inode is modified, which puts it on the dirty list of the
overlay's bdi.
Nearly every modification ends in ovl_copyattr(), so do it there. These do
not, so they mark the inode themselves:
- copy up, for the inode and for its parent
- ovl_instantiate(), for a new inode and for a new hard link
- atime updates
- shared writable mappings
- the removal of a stale "impure" xattr in readdir
Do not mark an unhashed inode. __mark_inode_dirty() would set its dirty
flags without queueing it, and every later call would return early. The
inode of a lower hardlink is unhashed until ovl_inode_update() hashes it.
The root inode is never hashed, a later patch takes care of it.
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 | 9 +++++++++
fs/overlayfs/dir.c | 3 +++
fs/overlayfs/file.c | 7 ++++++-
fs/overlayfs/inode.c | 4 +++-
fs/overlayfs/overlayfs.h | 20 ++++++++++++++++++++
fs/overlayfs/readdir.c | 1 +
fs/overlayfs/util.c | 12 +++++++++++-
7 files changed, 53 insertions(+), 3 deletions(-)
diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c
index e963701b4c87..44bc96aa3d5d 100644
--- a/fs/overlayfs/copy_up.c
+++ b/fs/overlayfs/copy_up.c
@@ -1199,6 +1199,15 @@ static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
err = ovl_link_up(&ctx);
if (!err && ovl_dentry_needs_data_copy_up_locked(dentry, flags))
err = ovl_copy_up_meta_inode_data(&ctx);
+ /*
+ * All three write to the upper inode, and there may be no later
+ * write that would mark the overlay inode dirty. The first two
+ * also add an entry to the upper inode of the parent, set its
+ * "impure" xattr and restore its timestamps.
+ */
+ ovl_mark_inode_dirty(d_inode(dentry));
+ if (parent)
+ ovl_mark_inode_dirty(d_inode(parent));
ovl_copy_up_end(dentry);
}
do_delayed_call(&done);
diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c
index 1194ccf981c3..a24326446659 100644
--- a/fs/overlayfs/dir.c
+++ b/fs/overlayfs/dir.c
@@ -331,6 +331,9 @@ static int ovl_instantiate(struct dentry *dentry, struct inode *inode,
inc_nlink(inode);
}
+ /* The upper inode was just created, or has got a new link */
+ ovl_mark_inode_dirty(inode);
+
if (tmpfile)
d_mark_tmpfile(tmpfile, inode);
diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c
index 7433220d4ad6..53c5131a0482 100644
--- a/fs/overlayfs/file.c
+++ b/fs/overlayfs/file.c
@@ -472,8 +472,13 @@ static int ovl_mmap(struct file *file, struct vm_area_struct *vma)
.cred = ovl_creds(file_inode(file)->i_sb),
.accessed = ovl_file_accessed,
};
+ int err;
- return backing_file_mmap(of->realfile, vma, &ctx);
+ 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 5b1a097ce543..d82c4bed5af2 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -691,6 +691,25 @@ static inline bool ovl_should_sync_upper_fs(struct ovl_fs *ofs)
return ovl_should_sync(ofs) && !ovl_tracks_dirty(ofs);
}
+/*
+ * The upper inode was modified through this overlay. Put the overlay inode on
+ * the dirty list of the overlay, where syncfs() and unmount find it.
+ *
+ * Do not look at the dirty flags of the upper inode here. They are cleared
+ * when the writeback of the upper inode starts, not when it completes.
+ *
+ * Do not mark an inode that is not hashed. __mark_inode_dirty() would set its
+ * dirty flags without putting it on a dirty list, and once the flags are set,
+ * later calls return early. The overlay inode of a lower hardlink is unhashed
+ * until ovl_inode_update() hashes it, and ovl_copy_up_one() marks it after
+ * that. The root inode is never hashed, so it is not tracked.
+ */
+static inline void ovl_mark_inode_dirty(struct inode *inode)
+{
+ if (ovl_tracks_dirty(OVL_FS(inode->i_sb)) && !inode_unhashed(inode))
+ mark_inode_dirty(inode);
+}
+
/*
* To avoid regressions in existing setups with overlay lower offline changes,
* we allow lower changes only if none of the new features are used.
@@ -878,6 +897,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/readdir.c b/fs/overlayfs/readdir.c
index 7d6f7f6022eb..f02c37600df6 100644
--- a/fs/overlayfs/readdir.c
+++ b/fs/overlayfs/readdir.c
@@ -724,6 +724,7 @@ static struct ovl_dir_cache *ovl_cache_get_impure(const struct path *path)
if (!ovl_want_write(dentry)) {
ovl_removexattr(ofs, ovl_dentry_upper(dentry),
OVL_XATTR_IMPURE);
+ ovl_mark_inode_dirty(inode);
ovl_drop_write(dentry);
}
ovl_clear_flag(OVL_IMPURE, inode);
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index 521717209b2e..a260c5a350e2 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -1492,7 +1492,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 +1516,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 modified through the
+ * overlay, so this is also where the overlay inode is marked dirty.
+ */
+void ovl_copyattr(struct inode *inode)
+{
+ __ovl_copyattr(inode);
+ ovl_mark_inode_dirty(inode);
+}
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 5/8] ovl: implement overlayfs' own ->write_inode operation
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
` (3 preceding siblings ...)
2026-11-07 19:19 ` [PATCH v5 4/8] ovl: mark the overlay inode dirty on modification Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 6/8] ovl: cache dirty overlayfs' inode Josef Bacik
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Chengguang Xu, Josef Bacik
From: Chengguang Xu <charliecgxu@tencent.com>
Sync dirty data and meta of upper inode in overlayfs' ->write_inode()
and redirty overlayfs' inode.
The overlay inode is only redirtied while the upper inode is open for
writing, dirty or under writeback. So a clean overlay inode means that
everything written through it has been written back, or could not be.
The periodic pass of the flusher does not write the upper inode, the
flusher of the upper filesystem does that. syncfs() waits for the data and
leaves the commit to ->sync_fs(), like sync_inodes_sb() does with for_sync.
The VFS ignores the return value of ->write_inode(), so record errors in
s_wb_err of the overlay for the next syncfs().
Signed-off-by: Chengguang Xu <charliecgxu@tencent.com>
[josef: everything after the first paragraph]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/overlayfs.h | 13 +++++++++
fs/overlayfs/super.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++
fs/overlayfs/util.c | 52 +++++++++++++++++++++++++++++++++
3 files changed, 140 insertions(+)
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index d82c4bed5af2..1d7b102a1127 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -512,6 +512,19 @@ const struct ovl_layer *ovl_layer_lower(struct dentry *dentry);
struct dentry *ovl_dentry_real(struct dentry *dentry);
struct dentry *ovl_i_dentry_upper(struct inode *inode);
struct inode *ovl_inode_upper(struct inode *inode);
+
+/* How ovl_write_upper_inode() writes an upper inode */
+enum ovl_write_mode {
+ /* Start the writeback and do not wait for it */
+ OVL_WRITE_START,
+ /* Wait for the data, ->sync_fs() is going to commit the metadata */
+ OVL_WRITE_WAIT,
+ /* A write_inode_now() that waits, for a caller that is not a sync */
+ OVL_WRITE_COMMIT,
+};
+
+int ovl_write_upper_inode(struct super_block *sb, struct inode *upper,
+ enum ovl_write_mode mode);
struct inode *ovl_inode_lower(struct inode *inode);
struct inode *ovl_inode_lowerdata(struct inode *inode);
struct inode *ovl_inode_real(struct inode *inode);
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index 22a8d4eb3b7c..41d0d37accda 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/backing-dev.h>
#include <linux/file.h>
#include <linux/fs_context.h>
@@ -314,12 +316,85 @@ 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?
+ *
+ * A task that has @upper open for writing may have it mapped, and overlayfs
+ * does not see stores through a mapping. The mapping holds the file open.
+ *
+ * Writeback that has been started but not completed, by us or by the flusher of
+ * the upper filesystem, shows in the page cache tags. It does not show in the
+ * dirty flags of the inode, which are cleared when writeback starts.
+ *
+ * So dirty flags that are clear say nothing. Dirty flags that are set do say
+ * that there is something left to write. @upper may not have been written at
+ * all, see ovl_write_inode(). And a write_inode_now() that does not wait does
+ * nothing if the flusher of the upper filesystem is writing @upper. A
+ * modification that was made during that pass is only recorded in the flags.
+ *
+ * i_writecount has to be read first. If it is seen at zero, the tag of a page
+ * that was dirtied through a mapping must be seen as well. The tag is set when
+ * the mapping goes away at the latest. i_writecount drops after that, when the
+ * file is released, with the full barrier of the last fput() in between.
+ */
+static bool ovl_upper_unfinished(struct inode *upper)
+{
+ struct address_space *mapping = upper->i_mapping;
+
+ return atomic_read_acquire(&upper->i_writecount) > 0 ||
+ mapping_tagged(mapping, PAGECACHE_TAG_DIRTY) ||
+ mapping_tagged(mapping, PAGECACHE_TAG_WRITEBACK) ||
+ (inode_state_read_once(upper) & (I_DIRTY | I_SYNC));
+}
+
+static enum ovl_write_mode ovl_write_mode(struct inode *inode,
+ struct writeback_control *wbc)
+{
+ if (wbc->sync_mode != WB_SYNC_ALL)
+ return OVL_WRITE_START;
+
+ /*
+ * syncfs() and unmount call ovl_sync_fs() next. While the overlay is
+ * being mounted, iput() writes a dirty inode because it cannot keep it.
+ * Nobody has asked for that inode to be durable yet.
+ */
+ if (wbc->for_sync || !(inode->i_sb->s_flags & SB_ACTIVE))
+ return OVL_WRITE_WAIT;
+
+ return OVL_WRITE_COMMIT;
+}
+
+static int ovl_write_inode(struct inode *inode, struct writeback_control *wbc)
+{
+ struct inode *upper = ovl_inode_upper(inode);
+ int ret = 0;
+
+ if (!upper)
+ return 0;
+
+ /*
+ * Leave periodic and background writeback to the flusher of the upper
+ * filesystem. @upper is on its dirty list, and it knows when the data
+ * is old enough to be written. All there is to do here is to find out
+ * whether the overlay inode can be clean again.
+ */
+ if (!wbc->for_kupdate && !wbc->for_background)
+ ret = ovl_write_upper_inode(inode->i_sb, upper,
+ ovl_write_mode(inode, wbc));
+
+ 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,
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index a260c5a350e2..d7b08a46cb76 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -14,6 +14,7 @@
#include <linux/fileattr.h>
#include <linux/uuid.h>
#include <linux/namei.h>
+#include <linux/pagemap.h>
#include <linux/ratelimit.h>
#include <linux/overflow.h>
#include "overlayfs.h"
@@ -1526,3 +1527,54 @@ void ovl_copyattr(struct inode *inode)
__ovl_copyattr(inode);
ovl_mark_inode_dirty(inode);
}
+
+/*
+ * Do what sync_inodes_sb() does to an inode: wait for the data, hand the inode
+ * to the filesystem, and leave it to ->sync_fs() to commit it. It gets there
+ * by setting for_sync, which write_inode_now() cannot be told about. If that
+ * waits, ext4 commits a transaction for every inode.
+ */
+static int ovl_sync_upper_inode(struct inode *upper)
+{
+ int err, err2;
+
+ err = filemap_write_and_wait(upper->i_mapping);
+
+ err2 = sync_inode_metadata(upper, 0);
+ /*
+ * That does nothing if the flusher of the upper filesystem is writing
+ * @upper, and the flusher may have looked at the inode before it was
+ * modified. Wait for the flusher in that case.
+ */
+ if (!err2 && (inode_state_read_once(upper) & (I_DIRTY_INODE | I_SYNC)))
+ err2 = sync_inode_metadata(upper, 1);
+
+ return err ? err : err2;
+}
+
+/*
+ * Write @upper for the overlay @sb.
+ *
+ * Nobody may be there to take the error. The VFS ignores what ->write_inode()
+ * returns when it walks the dirty list. Record the error in the superblock of
+ * the overlay, like mapping_set_error() does for a page that could not be
+ * written, so that the next syncfs() on the overlay returns it.
+ */
+int ovl_write_upper_inode(struct super_block *sb, struct inode *upper,
+ enum ovl_write_mode mode)
+{
+ int err;
+
+ /* sync_inodes_sb() does not wait for such a filesystem either */
+ if (upper->i_sb->s_iflags & SB_I_NO_DATA_INTEGRITY)
+ mode = OVL_WRITE_START;
+
+ if (mode == OVL_WRITE_WAIT)
+ err = ovl_sync_upper_inode(upper);
+ else
+ err = write_inode_now(upper, mode == OVL_WRITE_COMMIT);
+
+ if (err)
+ errseq_set(&sb->s_wb_err, err);
+ return err;
+}
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 6/8] ovl: cache dirty overlayfs' inode
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
` (4 preceding siblings ...)
2026-11-07 19:19 ` [PATCH v5 5/8] ovl: implement overlayfs' own ->write_inode operation Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 7/8] ovl: write the timestamps that lazytime defers Josef Bacik
2026-11-07 19:19 ` [PATCH v5 8/8] ovl: implement containerized syncfs for overlayfs Josef Bacik
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Josef Bacik
From: Chengguang Xu <cgxu519@mykernel.net>
Now drop overlayfs' inode will sync dirty data,
so we change to only drop clean inode.
The purpose of doing this is to keep compatible
behavior with before because without this change
dropping overlayfs inode will not trigger syncing
of underlying dirty inode.
Signed-off-by: Chengguang Xu <cgxu519@mykernel.net>
[josef: also keep an inode that is under writeback, rebase]
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 41d0d37accda..ccfbaf9d8797 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -388,11 +388,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 had its dirty flags cleared, and only I_SYNC
+ * pins it. Such an inode is not clean. ovl_write_inode() may be about to mark
+ * it dirty again, and marking an inode dirty that is being freed has no effect.
+ */
+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 [flat|nested] 9+ messages in thread
* [PATCH v5 7/8] ovl: write the timestamps that lazytime defers
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
` (5 preceding siblings ...)
2026-11-07 19:19 ` [PATCH v5 6/8] ovl: cache dirty overlayfs' inode Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
2026-11-07 19:19 ` [PATCH v5 8/8] ovl: implement containerized syncfs for overlayfs Josef Bacik
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Josef Bacik
With lazytime on the upper filesystem new timestamps stay in memory for up
to 12 hours, and syncfs() has to write them. So the overlay inode must not
become clean during that time, or syncfs() on the overlay would miss them.
It should not stay dirty for that long either, because the flusher would
visit it on every pass and it could not be reclaimed.
When overlayfs writes an upper inode, or looks at it in ->write_inode(),
and it has deferred timestamps, hand them to the upper filesystem. They
go out with that write, or with the next pass of the flusher of the upper
filesystem, and the overlay inode becomes clean after that.
This takes the benefit of lazytime away from inodes that are modified or
read through an overlay with fsync=relaxed. The flusher looks at the inode
of a file that is open for writing up to every 5 seconds.
A syncfs() that meets a freeze of the upper filesystem leaves the
timestamps to the freeze. One that meets a read-only remount of it that
fails leaves them to the next pass.
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
fs/overlayfs/overlayfs.h | 1 +
fs/overlayfs/super.c | 10 +++++++---
fs/overlayfs/util.c | 29 +++++++++++++++++++++++++++++
3 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h
index 1d7b102a1127..676520ff026f 100644
--- a/fs/overlayfs/overlayfs.h
+++ b/fs/overlayfs/overlayfs.h
@@ -523,6 +523,7 @@ enum ovl_write_mode {
OVL_WRITE_COMMIT,
};
+void ovl_flush_lazytime(struct super_block *sb, struct inode *upper);
int ovl_write_upper_inode(struct super_block *sb, struct inode *upper,
enum ovl_write_mode mode);
struct inode *ovl_inode_lower(struct inode *inode);
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index ccfbaf9d8797..f38c6dd2ae47 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -331,6 +331,7 @@ static int ovl_statfs(struct dentry *dentry, struct kstatfs *buf)
* all, see ovl_write_inode(). And a write_inode_now() that does not wait does
* nothing if the flusher of the upper filesystem is writing @upper. A
* modification that was made during that pass is only recorded in the flags.
+ * I_DIRTY_TIME is only left if ovl_flush_lazytime() could not get rid of it.
*
* i_writecount has to be read first. If it is seen at zero, the tag of a page
* that was dirtied through a mapping must be seen as well. The tag is set when
@@ -344,7 +345,7 @@ static bool ovl_upper_unfinished(struct inode *upper)
return atomic_read_acquire(&upper->i_writecount) > 0 ||
mapping_tagged(mapping, PAGECACHE_TAG_DIRTY) ||
mapping_tagged(mapping, PAGECACHE_TAG_WRITEBACK) ||
- (inode_state_read_once(upper) & (I_DIRTY | I_SYNC));
+ (inode_state_read_once(upper) & (I_DIRTY_ALL | I_SYNC));
}
static enum ovl_write_mode ovl_write_mode(struct inode *inode,
@@ -375,12 +376,15 @@ static int ovl_write_inode(struct inode *inode, struct writeback_control *wbc)
/*
* Leave periodic and background writeback to the flusher of the upper
* filesystem. @upper is on its dirty list, and it knows when the data
- * is old enough to be written. All there is to do here is to find out
- * whether the overlay inode can be clean again.
+ * is old enough to be written. All there is to do here is to hand over
+ * deferred timestamps, and to find out whether the overlay inode can be
+ * clean again.
*/
if (!wbc->for_kupdate && !wbc->for_background)
ret = ovl_write_upper_inode(inode->i_sb, upper,
ovl_write_mode(inode, wbc));
+ else
+ ovl_flush_lazytime(inode->i_sb, upper);
if (ovl_upper_unfinished(upper))
ovl_mark_inode_dirty(inode);
diff --git a/fs/overlayfs/util.c b/fs/overlayfs/util.c
index d7b08a46cb76..16ca33e2b794 100644
--- a/fs/overlayfs/util.c
+++ b/fs/overlayfs/util.c
@@ -1528,6 +1528,33 @@ void ovl_copyattr(struct inode *inode)
ovl_mark_inode_dirty(inode);
}
+/*
+ * With lazytime the upper filesystem keeps new timestamps in memory for up to
+ * 12 hours. syncfs() has to write them, so the overlay inode could not become
+ * clean for that long. It would be pinned, and the flusher would visit it on
+ * every pass. Do not wait. Hand the timestamps to the upper filesystem, which
+ * then writes the inode like after any other change.
+ *
+ * Like touch_atime(), leave a filesystem alone that is being frozen or
+ * remounted read-only. Both write the timestamps, and the overlay inode stays
+ * dirty until then, see ovl_upper_unfinished().
+ */
+void ovl_flush_lazytime(struct super_block *sb, struct inode *upper)
+{
+ struct vfsmount *mnt = ovl_upper_mnt(OVL_FS(sb));
+
+ if (!(inode_state_read_once(upper) & I_DIRTY_TIME))
+ return;
+
+ if (!sb_start_write_trylock(upper->i_sb))
+ return;
+ if (!mnt_get_write_access(mnt)) {
+ mark_inode_dirty_sync(upper);
+ mnt_put_write_access(mnt);
+ }
+ sb_end_write(upper->i_sb);
+}
+
/*
* Do what sync_inodes_sb() does to an inode: wait for the data, hand the inode
* to the filesystem, and leave it to ->sync_fs() to commit it. It gets there
@@ -1565,6 +1592,8 @@ int ovl_write_upper_inode(struct super_block *sb, struct inode *upper,
{
int err;
+ ovl_flush_lazytime(sb, upper);
+
/* sync_inodes_sb() does not wait for such a filesystem either */
if (upper->i_sb->s_iflags & SB_I_NO_DATA_INTEGRITY)
mode = OVL_WRITE_START;
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v5 8/8] ovl: implement containerized syncfs for overlayfs
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
` (6 preceding siblings ...)
2026-11-07 19:19 ` [PATCH v5 7/8] ovl: write the timestamps that lazytime defers Josef Bacik
@ 2026-11-07 19:19 ` Josef Bacik
7 siblings, 0 replies; 9+ messages in thread
From: Josef Bacik @ 2026-11-07 19:19 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Christian Brauner
Cc: Alexander Viro, Jan Kara, Christoph Hellwig, Chris Mason,
Chengguang Xu, linux-unionfs, linux-fsdevel, linux-kernel,
Chengguang Xu, Josef Bacik
From: Chengguang Xu <charliecgxu@tencent.com>
Now overlayfs can only sync own dirty inodes during syncfs,
so remove unnecessary sync_filesystem() on upper file system.
Do that with fsync=relaxed only. Call sync_fs_and_bdev() on the upper
filesystem instead, which is what sync_filesystem() does after it has
written the inodes. Do the same for a read-only remount.
The root inode of an overlay is not hashed and cannot be on a dirty list,
so write its upper inode every time.
Upper inodes that have no overlay inode, like whiteouts and the work
directories, are not tracked. It is up to ->sync_fs() and the block device
to get them to disk. ext4, xfs and btrfs log all metadata, and ext4
without a journal keeps it in the buffers of the block device.
With many containers on one upper filesystem, stopping one of them no
longer writes out the dirty data of all the others.
Signed-off-by: Chengguang Xu <charliecgxu@tencent.com>
[josef: everything after the first paragraph, no wait_sb_inodes() on the
upper filesystem]
Signed-off-by: Josef Bacik <josef@toxicpanda.com>
---
Documentation/filesystems/overlayfs.rst | 2 +-
fs/overlayfs/params.c | 14 +++----------
fs/overlayfs/super.c | 36 +++++++++++++++++++++++++--------
3 files changed, 32 insertions(+), 20 deletions(-)
diff --git a/Documentation/filesystems/overlayfs.rst b/Documentation/filesystems/overlayfs.rst
index 1a59500834a1..34fc4e6f8a84 100644
--- a/Documentation/filesystems/overlayfs.rst
+++ b/Documentation/filesystems/overlayfs.rst
@@ -830,7 +830,7 @@ Traditionally, syncfs(2) and unmount of an overlay sync the entire upper
filesystem, including the dirty data of other overlays that share it.
Overlayfs can be told to only write back the files that were modified
through the overlay, and then ask the upper filesystem to commit its
-metadata.
+metadata and write its block device.
On traditional local filesystems with a single journal (e.g. ext4, xfs),
fsync on a file also persists the parent directory changes, because they
diff --git a/fs/overlayfs/params.c b/fs/overlayfs/params.c
index 549fc1e83ac9..6de99e79eb6b 100644
--- a/fs/overlayfs/params.c
+++ b/fs/overlayfs/params.c
@@ -774,22 +774,14 @@ static int ovl_reconfigure(struct fs_context *fc)
{
struct super_block *sb = fc->root->d_sb;
struct ovl_fs *ofs = OVL_FS(sb);
- struct super_block *upper_sb;
- int ret = 0;
if (!(fc->sb_flags & SB_RDONLY) && ovl_force_readonly(ofs))
return -EROFS;
- if (fc->sb_flags & SB_RDONLY && !sb_rdonly(sb)) {
- upper_sb = ovl_upper_mnt(ofs)->mnt_sb;
- if (ovl_should_sync(ofs)) {
- down_read(&upper_sb->s_umount);
- ret = sync_filesystem(upper_sb);
- up_read(&upper_sb->s_umount);
- }
- }
+ if (fc->sb_flags & SB_RDONLY && !sb_rdonly(sb) && ovl_should_sync(ofs))
+ return sync_filesystem(sb);
- return ret;
+ return 0;
}
static const struct fs_context_operations ovl_context_ops = {
diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c
index f38c6dd2ae47..72f5d81aff6a 100644
--- a/fs/overlayfs/super.c
+++ b/fs/overlayfs/super.c
@@ -270,18 +270,38 @@ 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_should_sync_upper_fs(ofs)) {
+ /* 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;
+ }
+
+ /*
+ * sync_filesystem() has already written the dirty inodes of this
+ * overlay through ovl_write_inode(). What is left are the last two
+ * steps of sync_filesystem() on the upper filesystem. They have to
+ * take care of the upper inodes that have no overlay inode, like
+ * whiteouts and the work directories. Do not write the inodes that
+ * others have dirtied on the upper filesystem.
+ *
+ * The root inode is not hashed, so it is never on the dirty list.
+ * Write its upper inode every time. sb->s_root is gone during an
+ * unmount. An error ends up in s_wb_err, like for any other inode.
+ */
+ ovl_write_upper_inode(sb, d_inode(ovl_upper_mnt(ofs)->mnt_root),
+ wait ? OVL_WRITE_WAIT : OVL_WRITE_START);
+
down_read(&upper_sb->s_umount);
- ret = sync_filesystem(upper_sb);
+ ret = sync_fs_and_bdev(upper_sb, wait);
up_read(&upper_sb->s_umount);
return ret;
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-11-07 19:19 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-07 19:19 [PATCH v5 0/8] ovl: only sync what was written through the overlay Josef Bacik
2026-11-07 19:19 ` [PATCH v5 1/8] ovl: expand fsync mount options Josef Bacik
2026-11-07 19:19 ` [PATCH v5 2/8] fs: add sync_fs_and_bdev() Josef Bacik
2026-11-07 19:19 ` [PATCH v5 3/8] ovl: setup overlayfs' private bdi Josef Bacik
2026-11-07 19:19 ` [PATCH v5 4/8] ovl: mark the overlay inode dirty on modification Josef Bacik
2026-11-07 19:19 ` [PATCH v5 5/8] ovl: implement overlayfs' own ->write_inode operation Josef Bacik
2026-11-07 19:19 ` [PATCH v5 6/8] ovl: cache dirty overlayfs' inode Josef Bacik
2026-11-07 19:19 ` [PATCH v5 7/8] ovl: write the timestamps that lazytime defers Josef Bacik
2026-11-07 19:19 ` [PATCH v5 8/8] ovl: implement containerized syncfs for overlayfs Josef Bacik
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®