* [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state
@ 2026-09-15 8:32 Hongling Zeng
2026-09-15 8:32 ` [PATCH v13 1/7] ntfs: fix volume flag update races Hongling Zeng
` (9 more replies)
0 siblings, 10 replies; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng
Hi all,
The fs/ntfs runtime metadata-corruption paths only record the in-memory
NVolErrors() flag, and the dirty-bit persistence used to race with
ntfs_sync_fs(): a volume could end up with a clean on-disk dirty flag
despite modification or recorded corruption, so chkdsk would not run on
the next mount. This series fixes that.
1/6 makes the volume flag read-modify-write atomic under the
$Volume mrec_lock;
2/6 marks the volume dirty unconditionally on metadata changes,
dropping the racy caller-side checks in file.c and namei.c;
3/6 derives the on-disk dirty bit from the recorded error state at
the persistence points (sync_fs, remount-ro, put_super) and never
writes a hibernated volume;
4/6 persists the dirty state after the final put_super() commits so
late errors cannot unmount clean;
5/6 stops ntfs_sync_fs() from clearing VOLUME_IS_DIRTY: the clearing
moves to the quiescent transitions, a recorded error state is
still persisted at sync time, and sync now reports writeback and
flush errors instead of discarding them;
6/6 checks the dirty-state commit on remount and unmount.
7/7 ntfs: fail remount on sync errors and keep the dirty bit on
SB_FORCE
Changes in this revision, from the review:
- 5/6: with the clearing gone from the sync path, a recorded error
state is persisted without ever clearing the bit, and
sync_blockdev() and blkdev_issue_flush() are both called with the
first error returned.
- The IOCB_NOWAIT behavior and the per-operation $Volume mrec_lock
acquisition are outside the scope of this series. The series keeps
the unconditional ntfs_set_volume_flags() call to preserve the
ordering that marks the volume dirty before the metadata
modification; a RWF_NOWAIT write still blocks in the marking when
the volume looks clean, as it already did on the base. The
non-blocking and contended-lock handling (mutex_trylock, GFP_NOWAIT)
will be addressed in a separate follow-up patch.
Hongling Zeng (4):
ntfs: fix volume flag update races
ntfs: set the volume dirty bit unconditionally on metadata changes
ntfs: sync the volume dirty bit with the recorded error state
ntfs: persist the dirty state after the final put_super() commits
fs/ntfs/file.c | 20 ++---
fs/ntfs/namei.c | 24 ++----
fs/ntfs/ntfs.h | 1 -
fs/ntfs/super.c | 191 ++++++++++++++++++++++++++++++++++++-----------
fs/ntfs/volume.h | 4 +
5 files changed, 171 insertions(+), 69 deletions(-)
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 1/7] ntfs: fix volume flag update races
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 20:23 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 2/7] ntfs: set the volume dirty bit unconditionally on metadata changes Hongling Zeng
` (8 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, stable
ntfs_set_volume_flags() and ntfs_clear_volume_flags() both read
vol->vol_flags outside any lock to compute the new value before handing
it to ntfs_write_volume_flags(), which only takes ni->mrec_lock around
the actual write. The read-modify-write is therefore not atomic, and two
concurrent callers can lose an update: ntfs_sync_fs() may derive a
"clean" value from vol->vol_flags while a writer concurrently records an
error and sets VOLUME_IS_DIRTY; the locked write then silently
overwrites the freshly-set dirty bit. The on-disk volume looks clean
despite the recorded errors, so chkdsk will not run on the next mount
and corrupted metadata can persist.
Fix by moving the read-modify-write inside the mrec_lock: pass the bits
to set and to clear separately, and combine them with the current flag
state under the lock inside ntfs_write_volume_flags(). The set/clear
helpers pass only the bits to modify, not the complete flag state. The
bit manipulation is done on CPU-endian values, and the result is
converted back to little-endian before storing it. The wrappers keep
their signatures so callers are unchanged.
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/super.c | 63 +++++++++++++++++++++++++++++++++++--------------
1 file changed, 45 insertions(+), 18 deletions(-)
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index f4a73e45773d..6ba19986a598 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -353,31 +353,45 @@ void ntfs_handle_error(struct super_block *sb)
}
/*
- * ntfs_write_volume_flags - write new flags to the volume information flags
+ * ntfs_write_volume_flags - apply flag changes to the volume information flags
* @vol: ntfs volume on which to modify the flags
- * @flags: new flags value for the volume information flags
+ * @set_bits: bits to set in the volume information flags
+ * @clear_bits: bits to clear in the volume information flags
*
* Internal function. You probably want to use ntfs_{set,clear}_volume_flags()
* instead (see below).
*
- * Replace the volume information flags on the volume @vol with the value
- * supplied in @flags. Note, this overwrites the volume information flags, so
- * make sure to combine the flags you want to modify with the old flags and use
- * the result when calling ntfs_write_volume_flags().
+ * Combine @set_bits and @clear_bits with the current in-memory flag state and
+ * write the result back. The set/clear helpers pass only the bits to modify,
+ * not the complete flag state. The read-modify-write happens under
+ * ni->mrec_lock so that concurrent set/clear operations cannot lose updates.
+ * All bit manipulation is done on CPU-endian values, and the result is
+ * converted back to little-endian before storing it.
*
* Return 0 on success and -errno on error.
*/
-static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags)
+static int ntfs_write_volume_flags(struct ntfs_volume *vol,
+ const __le16 set_bits, const __le16 clear_bits,
+ const bool skip_if_errors)
{
struct ntfs_inode *ni = NTFS_I(vol->vol_ino);
struct volume_information *vi;
struct ntfs_attr_search_ctx *ctx;
+ u16 flags;
int err;
- ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
- le16_to_cpu(vol->vol_flags), le16_to_cpu(flags));
mutex_lock(&ni->mrec_lock);
- if (vol->vol_flags == flags)
+
+ if (skip_if_errors && NVolErrors(vol))
+ goto done;
+
+ flags = le16_to_cpu(vol->vol_flags);
+ flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK);
+ flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK));
+ ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
+ le16_to_cpu(vol->vol_flags), flags);
+
+ if (le16_to_cpu(vol->vol_flags) == flags)
goto done;
ctx = ntfs_attr_get_search_ctx(ni, NULL);
@@ -393,7 +407,7 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags)
vi = (struct volume_information *)((u8 *)ctx->attr +
le16_to_cpu(ctx->attr->data.resident.value_offset));
- vol->vol_flags = vi->flags = flags;
+ vol->vol_flags = vi->flags = cpu_to_le16(flags);
mark_mft_record_dirty(ctx->ntfs_ino);
ntfs_attr_put_search_ctx(ctx);
done:
@@ -414,13 +428,14 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags)
* @flags: flags to set on the volume
*
* Set the bits in @flags in the volume information flags on the volume @vol.
+ * The bits are combined with the current flag state under the lock in
+ * ntfs_write_volume_flags(), so concurrent updates are not lost.
*
* Return 0 on success and -errno on error.
*/
int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
{
- flags &= VOLUME_FLAGS_MASK;
- return ntfs_write_volume_flags(vol, vol->vol_flags | flags);
+ return ntfs_write_volume_flags(vol, flags, 0, false);
}
/*
@@ -429,14 +444,27 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
* @flags: flags to clear on the volume
*
* Clear the bits in @flags in the volume information flags on the volume @vol.
+ * The bits are combined with the current flag state under the lock in
+ * ntfs_write_volume_flags(), so concurrent updates are not lost.
*
* Return 0 on success and -errno on error.
*/
int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags)
{
- flags &= VOLUME_FLAGS_MASK;
- flags = vol->vol_flags & cpu_to_le16(~le16_to_cpu(flags));
- return ntfs_write_volume_flags(vol, flags);
+ return ntfs_write_volume_flags(vol, 0, flags, false);
+}
+
+/*
+ * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist
+ * @vol: ntfs volume whose dirty bit should be cleared
+ *
+ * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so
+ * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been
+ * recorded.
+ */
+static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol)
+{
+ return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true);
}
int ntfs_write_volume_label(struct ntfs_volume *vol, char *label)
@@ -1858,8 +1886,7 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
return 0;
/* If there are some dirty buffers in the bdev inode */
- if (!NVolErrors(vol) &&
- ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) {
+ if (ntfs_clear_volume_dirty_if_no_errors(vol)) {
ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk.");
err = -EIO;
}
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 2/7] ntfs: set the volume dirty bit unconditionally on metadata changes
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
2026-09-15 8:32 ` [PATCH v13 1/7] ntfs: fix volume flag update races Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 20:23 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state Hongling Zeng
` (7 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, Baolin Liu, stable
The callers in file.c and namei.c skip ntfs_set_volume_flags() when
the in-memory vol_flags already show VOLUME_IS_DIRTY, but that check
runs without any lock: if it observes the bit set and ntfs_sync_fs()
clears it under the mrec_lock before the caller's metadata update
completes, the set is skipped and the volume can end up clean on disk
despite the modification, so chkdsk will not run on the next mount.
Drop the caller-side checks and call ntfs_set_volume_flags()
unconditionally: ntfs_write_volume_flags() already skips the write
under the mrec_lock when the combined value is unchanged. That
unconditional call costs one mrec_lock acquisition per metadata
operation even in the already-dirty steady state; it cannot be
avoided, because deciding to skip the call without the lock is itself
what allows a concurrent ntfs_sync_fs() clear to lose the set.
The IOCB_NOWAIT path in ntfs_file_write_iter() goes through the same
sleeping call: a RWF_NOWAIT write can block in the marking, as it
already could before this change whenever the volume appeared clean.
Giving that path a non-blocking variant is left as follow-up work.
The callers keep the pre-existing behavior of proceeding when the
marking fails, so the dirty bit remains best-effort.
This closes the variant where the set is skipped outright. A clear
for a concurrent, error-free sync can still land between the set and
the end of the metadata operation; that mark-at-start lifecycle is
pre-existing and is not changed by this patch.
Reported-by: Baolin Liu <liubaolin@kylinos.cn>
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/file.c | 20 +++++++++++---------
fs/ntfs/namei.c | 24 ++++++++----------------
2 files changed, 19 insertions(+), 25 deletions(-)
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index 007d1614b9ac..cfc7b36b7dff 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -325,8 +325,7 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
goto out;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
if (ia_valid & ATTR_SIZE) {
err = ntfs_setattr_size(vi, attr);
@@ -620,8 +619,13 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
goto out_lock;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ /*
+ * The volume must be marked dirty before the modification is made,
+ * without an unlocked check of the in-memory flag: ntfs_sync_fs()
+ * can clear the bit concurrently and the modification would then
+ * land on a volume that is clean on disk.
+ */
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
pos = iocb->ki_pos;
count = ret;
@@ -1153,11 +1157,9 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le
return err;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY)) {
- err = ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
- if (err)
- return err;
- }
+ err = ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ if (err)
+ return err;
old_size = i_size_read(vi);
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index fdf52fac4329..3e0adb9a0ea4 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -757,8 +757,7 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir,
return err;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, NULL, 0);
kmem_cache_free(ntfs_name_cache, uname);
@@ -1032,8 +1031,7 @@ static int ntfs_unlink(struct inode *dir, struct dentry *dentry)
return err;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
err = ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true);
if (err)
@@ -1076,8 +1074,7 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
return ERR_PTR(err);
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0, NULL, 0);
kmem_cache_free(ntfs_name_cache, uname);
@@ -1118,8 +1115,7 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry)
return err;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
err = ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true);
if (err)
@@ -1305,8 +1301,7 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
new_dir_first = is_subdir(new_dentry->d_parent,
old_dentry->d_parent);
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
mutex_lock_nested(&old_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
if (new_ni)
@@ -1429,8 +1424,7 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
goto out;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
ni = __ntfs_create(idmap, dir, usrc, usrc_len, S_IFLNK | 0777, 0,
symname, symlen);
@@ -1474,8 +1468,7 @@ static int ntfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
return err;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
switch (mode & S_IFMT) {
case S_IFCHR:
@@ -1521,8 +1514,7 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir,
return -ENOMEM;
}
- if (!(vol->vol_flags & VOLUME_IS_DIRTY))
- ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
+ ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
ihold(vi);
mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
2026-09-15 8:32 ` [PATCH v13 1/7] ntfs: fix volume flag update races Hongling Zeng
2026-09-15 8:32 ` [PATCH v13 2/7] ntfs: set the volume dirty bit unconditionally on metadata changes Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 20:23 ` liubaolin
2026-09-16 21:17 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 4/7] ntfs: persist the dirty state after the final put_super() commits Hongling Zeng
` (6 subsequent siblings)
9 siblings, 2 replies; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, stable
The runtime metadata-corruption paths in fs/ntfs only record the
in-memory NVolErrors() flag; whether VOLUME_IS_DIRTY ever reaches disk
depends on ntfs_set_volume_flags() being called by some other path,
which for most error sites never happens. A volume can therefore
unmount with a clean on-disk flag despite recorded corruption, and
chkdsk will not run on the next mount.
Persisting the dirty bit from the error paths themselves does not work:
they run under a wide variety of ntfs locks, and the dirty-bit write
takes the $Volume mrec_lock and maps the $Volume mft record, which on
an $MFT page-cache miss takes the $MFT runlist lock for writing. That
is enough to self-deadlock or form ABBA cycles from several of them:
the $MFT extend undo paths hold the $MFT runlist lock and then take
vol->lcnbmp_lock inside ntfs_cluster_free(); the cluster allocation and
free rollback paths hold vol->lcnbmp_lock; and the whole mft record
allocation tree is reachable from ntfs_write_volume_label()'s
attribute-list maintenance while it holds the $Volume mrec_lock itself.
Instead, make the persistence a property of the sync paths, which run
without ntfs locks held. The new ntfs_sync_volume_dirty_state() sets
VOLUME_IS_DIRTY when NVolErrors() is recorded and clears it otherwise,
evaluating the error flag under the $Volume mrec_lock. It is called
from ntfs_sync_fs(), from the remount-to-read-only path of
ntfs_reconfigure(), and from ntfs_put_super(), which previously
evaluated NVolErrors() outside the lock before clearing the dirty bit
unconditionally, and which now also persists the dirty bit for volumes
with recorded errors so they unmount with chkdsk scheduled. The
ntfs_clear_volume_flags() wrapper, whose last callers this patch
replaces, has no users left and is removed.
The guarantee this provides is eventual, not instantaneous: the error
paths record NVolErrors() with a lock-free set_bit(), so a persistence
point that evaluates the flag just before an error is recorded can
still leave the on-disk bit clean until the next one. This is sound
because NVolErrors() is sticky for the lifetime of the mount and every
persistence point re-derives the on-disk bit from it; the last one,
ntfs_put_super(), runs after evict_inodes() on a quiesced filesystem,
so a volume that is read-write at unmount time cannot unmount clean.
A volume that is already read-only when the error is recorded
(errors=remount-ro flips the superblock on the first error, as does an
earlier remount-ro) has no persistence point left and keeps whatever
on-disk bit it had; that behaviour is unchanged. The residual window
is a crash between the error and the next persistence point.
The persistence paths never write a hibernated volume: resuming Windows
from a modified image corrupts it. Record the mount-time hibernation
verdict in the new NV_Hibernated volume flag and make
ntfs_sync_volume_dirty_state() a no-op while it is set, so the dirty
bit is left exactly as it is on disk and only the in-memory error
state is kept. Without this, an rw mount of a hibernated volume with
the default errors=continue would gain a filesystem-internal write on
the first sync, remount or unmount. Other writes to such a mount,
like the mount-time logfile emptying, are pre-existing and unchanged.
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/ntfs.h | 1 -
fs/ntfs/super.c | 129 ++++++++++++++++++++++++++++++++---------------
fs/ntfs/volume.h | 4 ++
3 files changed, 93 insertions(+), 41 deletions(-)
diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h
index 45f77848a9cf..a5cd5493c501 100644
--- a/fs/ntfs/ntfs.h
+++ b/fs/ntfs/ntfs.h
@@ -219,7 +219,6 @@ struct option_t {
};
extern const struct option_t on_errors_arr[];
int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags);
-int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags);
int ntfs_write_volume_label(struct ntfs_volume *vol, char *label);
/* From fs/ntfs/mst.c */
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 6ba19986a598..733565953302 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -262,6 +262,8 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
return 0;
}
+static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol);
+
static int ntfs_reconfigure(struct fs_context *fc)
{
struct super_block *sb = fc->root->d_sb;
@@ -312,10 +314,24 @@ static int ntfs_reconfigure(struct fs_context *fc)
}
} else if (!sb_rdonly(sb) && (fc->sb_flags & SB_RDONLY)) {
/* Remounting read-only. */
- if (!NVolErrors(vol)) {
- if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY))
- ntfs_warning(sb,
- "Failed to clear dirty bit in volume information flags. Run chkdsk.");
+ /*
+ * With errors recorded the dirty bit is set rather than
+ * cleared, and it is committed right away: the VFS does
+ * not sync the filesystem during a remount, and once the
+ * remount succeeds no further persistence point exists -
+ * ntfs_sync_fs() is only ever invoked for read-write
+ * superblocks (all its VFS callers skip read-only ones)
+ * and ntfs_put_super() skips them, so the only remaining
+ * write would be the evict-time commit at unmount, which
+ * a crash never reaches. An error recorded only after
+ * the remount is still never persisted.
+ */
+ if (ntfs_sync_volume_dirty_state(vol)) {
+ ntfs_warning(sb,
+ "Failed to update dirty bit in volume information flags. Run chkdsk.");
+ } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
+ ntfs_commit_inode(vol->vol_ino);
+ blkdev_issue_flush(sb->s_bdev);
}
}
@@ -357,9 +373,10 @@ void ntfs_handle_error(struct super_block *sb)
* @vol: ntfs volume on which to modify the flags
* @set_bits: bits to set in the volume information flags
* @clear_bits: bits to clear in the volume information flags
+ * @dirty_if_errors: force VOLUME_IS_DIRTY on when NVolErrors() is set
*
* Internal function. You probably want to use ntfs_{set,clear}_volume_flags()
- * instead (see below).
+ * or ntfs_sync_volume_dirty_state() instead (see below).
*
* Combine @set_bits and @clear_bits with the current in-memory flag state and
* write the result back. The set/clear helpers pass only the bits to modify,
@@ -368,11 +385,18 @@ void ntfs_handle_error(struct super_block *sb)
* All bit manipulation is done on CPU-endian values, and the result is
* converted back to little-endian before storing it.
*
+ * When @dirty_if_errors is true and errors have been recorded on @vol,
+ * VOLUME_IS_DIRTY is forced on after the requested changes. NVolErrors() is
+ * evaluated under the same mrec_lock, which orders this against other
+ * locked flag updates; the runtime error paths themselves record the flag
+ * lock-free, so see ntfs_sync_volume_dirty_state() for the guarantee this
+ * provides against them.
+ *
* Return 0 on success and -errno on error.
*/
static int ntfs_write_volume_flags(struct ntfs_volume *vol,
const __le16 set_bits, const __le16 clear_bits,
- const bool skip_if_errors)
+ const bool dirty_if_errors)
{
struct ntfs_inode *ni = NTFS_I(vol->vol_ino);
struct volume_information *vi;
@@ -382,12 +406,11 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol,
mutex_lock(&ni->mrec_lock);
- if (skip_if_errors && NVolErrors(vol))
- goto done;
-
flags = le16_to_cpu(vol->vol_flags);
flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK);
flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK));
+ if (dirty_if_errors && NVolErrors(vol))
+ flags |= le16_to_cpu(VOLUME_IS_DIRTY);
ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
le16_to_cpu(vol->vol_flags), flags);
@@ -439,31 +462,43 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
}
/*
- * ntfs_clear_volume_flags - clear bits in the volume information flags
- * @vol: ntfs volume on which to modify the flags
- * @flags: flags to clear on the volume
+ * ntfs_sync_volume_dirty_state - persist the dirty bit per the error state
+ * @vol: ntfs volume whose dirty bit to persist
*
- * Clear the bits in @flags in the volume information flags on the volume @vol.
- * The bits are combined with the current flag state under the lock in
- * ntfs_write_volume_flags(), so concurrent updates are not lost.
+ * Set VOLUME_IS_DIRTY if errors have been recorded on @vol and clear it
+ * otherwise, under the $Volume mrec_lock.
*
- * Return 0 on success and -errno on error.
- */
-int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags)
-{
- return ntfs_write_volume_flags(vol, 0, flags, false);
-}
-
-/*
- * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist
- * @vol: ntfs volume whose dirty bit should be cleared
+ * The guarantee this provides is eventual, not instantaneous: the runtime
+ * error paths record NVolErrors() with a lock-free set_bit(), so a
+ * persistence point that evaluates the flag just before an error is
+ * recorded can still leave the on-disk bit clean. This is sound because
+ * NVolErrors() is sticky (nothing clears it for the lifetime of the mount)
+ * and every persistence point re-derives the on-disk bit from it; the
+ * last one, ntfs_put_super(), runs after evict_inodes() on a quiesced
+ * filesystem, so a volume that is read-write at unmount time cannot
+ * unmount clean. A volume that is already read-only when the error is
+ * recorded (errors=remount-ro flips the superblock on the first error,
+ * as does an earlier remount-ro) has no persistence point left and
+ * keeps whatever on-disk bit it had; that behaviour is unchanged. The
+ * residual window is a crash between the error and the next
+ * persistence point.
+ *
+ * This is the single point that persists the in-memory error state to disk.
+ * The runtime error paths only record NVolErrors() because they run under a
+ * variety of ntfs locks the dirty-bit write cannot be taken under (runlist
+ * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first
+ * ntfs_sync_fs(), a remount, or the unmount then persists the flag here.
+ *
+ * A hibernated volume is not written from these persistence paths:
+ * resuming Windows from a modified image corrupts it, so the dirty bit
+ * is left as it is on disk and only the in-memory error state is kept.
*
- * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so
- * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been
- * recorded.
+ * Return 0 on success and -errno on error.
*/
-static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol)
+static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol)
{
+ if (NVolHibernated(vol))
+ return 0;
return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true);
}
@@ -1615,6 +1650,11 @@ static bool load_system_files(struct ntfs_volume *vol)
ntfs_error(sb, "%s. Mounting read-only%s", es1, es2);
}
NVolSetErrors(vol);
+ /*
+ * Remember it for the lifetime of the mount: see
+ * ntfs_sync_volume_dirty_state().
+ */
+ NVolSetHibernated(vol);
}
/* If (still) a read-write mount, empty the logfile. */
@@ -1772,22 +1812,31 @@ static void ntfs_put_super(struct super_block *sb)
ntfs_commit_inode(vol->mft_ino);
/*
- * If a read-write mount and no volume errors have occurred, mark the
- * volume clean. Also, re-commit all affected inodes.
+ * If a read-write mount, persist the error state in the volume flags:
+ * mark the volume clean if no volume errors have occurred, and make
+ * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
+ * next mount. Also, re-commit all affected inodes.
*/
if (!sb_rdonly(sb)) {
+ if (ntfs_sync_volume_dirty_state(vol)) {
+ ntfs_warning(sb,
+ "Failed to sync dirty bit in volume information flags. Run chkdsk.");
+ } else if (NVolErrors(vol)) {
+ /*
+ * The dirty bit is on disk now; only warn when the
+ * sync actually succeeded, or this message would
+ * contradict the one above.
+ */
+ ntfs_warning(sb,
+ "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
+ }
+ /* Commits the updated volume flags if they were written. */
+ ntfs_commit_inode(vol->vol_ino);
if (!NVolErrors(vol)) {
- if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY))
- ntfs_warning(sb,
- "Failed to clear dirty bit in volume information flags. Run chkdsk.");
- ntfs_commit_inode(vol->vol_ino);
ntfs_commit_inode(vol->root_ino);
if (vol->mftmirr_ino)
ntfs_commit_inode(vol->mftmirr_ino);
ntfs_commit_inode(vol->mft_ino);
- } else {
- ntfs_warning(sb,
- "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
}
}
@@ -1886,8 +1935,8 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
return 0;
/* If there are some dirty buffers in the bdev inode */
- if (ntfs_clear_volume_dirty_if_no_errors(vol)) {
- ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk.");
+ if (ntfs_sync_volume_dirty_state(vol)) {
+ ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk.");
err = -EIO;
}
sync_inodes_sb(sb);
diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
index bc85a9592245..c7cd27b6dc1a 100644
--- a/fs/ntfs/volume.h
+++ b/fs/ntfs/volume.h
@@ -181,6 +181,8 @@ struct ntfs_volume {
* Windows-reserved names (CON, AUX, NUL, COM1,
* LPT1, etc.) or invalid characters.
*
+ * NV_Hibernated Windows is hibernated on the volume; the sync
+ * paths must not write the volume flags.
* NV_Discard Issue discard/TRIM commands for freed clusters.
* NV_DisableSparse Disable creation of sparse regions.
* NV_NativeSymlinkRel Translate absolute Windows reparse targets (native_symlink=rel).
@@ -199,6 +201,7 @@ enum {
NV_ShowHiddenFiles,
NV_HideDotFiles,
NV_CheckWindowsNames,
+ NV_Hibernated,
NV_Discard,
NV_DisableSparse,
NV_NativeSymlinkRel,
@@ -237,6 +240,7 @@ DEFINE_NVOL_BIT_OPS(SysImmutable)
DEFINE_NVOL_BIT_OPS(ShowHiddenFiles)
DEFINE_NVOL_BIT_OPS(HideDotFiles)
DEFINE_NVOL_BIT_OPS(CheckWindowsNames)
+DEFINE_NVOL_BIT_OPS(Hibernated)
DEFINE_NVOL_BIT_OPS(Discard)
DEFINE_NVOL_BIT_OPS(DisableSparse)
DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 4/7] ntfs: persist the dirty state after the final put_super() commits
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (2 preceding siblings ...)
2026-09-15 8:32 ` [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 5/7] ntfs: do not clear the volume dirty bit during sync Hongling Zeng
` (5 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, Baolin Liu, stable
The just-in-case mftmirr/mft commits and the final write_inode_now()
in ntfs_put_super() can record NVolErrors() after the dirty state has
been persisted, so errors from those points would leave the volume
unmounted with a clean on-disk dirty bit - contradicting the "cannot
unmount clean" guarantee ntfs_sync_volume_dirty_state() is meant to
provide.
Move the persistence to the end of ntfs_put_super(): keep the gated
re-commits and the tail commits where they are, run
ntfs_sync_volume_dirty_state() and the $Volume commit after the last
write_inode_now(), and release vol->vol_ino only after the sync.
The release order of the special inodes matters for the $Volume
commit: writing the $Volume record mirrors it through
ntfs_sync_mft_mirror() (record number 3 is below vol->mftmirr_size),
which fails with -EIO and leaves the mirror stale once
vol->mftmirr_ino is gone, so the mirror inode is released only after
that commit. vol->vol_ino is then put before vol->mft_ino is dropped:
if the commit failed before it could clear the dirty flag,
ntfs_evict_big_inode() commits the inode again on its way out, and
__ntfs_write_inode() resolves the runlist through vol->mft_ino.
Reported-by: Baolin Liu <liubaolin@kylinos.cn>
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/super.c | 75 ++++++++++++++++++++++++++++++++++---------------
1 file changed, 52 insertions(+), 23 deletions(-)
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 733565953302..3574c224fe28 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -1812,26 +1812,13 @@ static void ntfs_put_super(struct super_block *sb)
ntfs_commit_inode(vol->mft_ino);
/*
- * If a read-write mount, persist the error state in the volume flags:
- * mark the volume clean if no volume errors have occurred, and make
- * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
- * next mount. Also, re-commit all affected inodes.
+ * If a read-write mount, re-commit all affected inodes once more.
+ * The dirty state itself is persisted at the end of ntfs_put_super(),
+ * after the last commits and the final write_inode_now(): those can
+ * still record errors via __ntfs_write_inode(), and the sync must
+ * evaluate NVolErrors() with the last setter already run.
*/
if (!sb_rdonly(sb)) {
- if (ntfs_sync_volume_dirty_state(vol)) {
- ntfs_warning(sb,
- "Failed to sync dirty bit in volume information flags. Run chkdsk.");
- } else if (NVolErrors(vol)) {
- /*
- * The dirty bit is on disk now; only warn when the
- * sync actually succeeded, or this message would
- * contradict the one above.
- */
- ntfs_warning(sb,
- "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
- }
- /* Commits the updated volume flags if they were written. */
- ntfs_commit_inode(vol->vol_ino);
if (!NVolErrors(vol)) {
ntfs_commit_inode(vol->root_ino);
if (vol->mftmirr_ino)
@@ -1840,9 +1827,6 @@ static void ntfs_put_super(struct super_block *sb)
}
}
- iput(vol->vol_ino);
- vol->vol_ino = NULL;
-
/* NTFS 3.0+ specific clean up. */
if (vol->major_ver >= 3) {
if (vol->extend_ino) {
@@ -1872,8 +1856,6 @@ static void ntfs_put_super(struct super_block *sb)
/* Re-commit the mft mirror and mft just in case. */
ntfs_commit_inode(vol->mftmirr_ino);
ntfs_commit_inode(vol->mft_ino);
- iput(vol->mftmirr_ino);
- vol->mftmirr_ino = NULL;
}
/*
* We should have no dirty inodes left, due to
@@ -1883,6 +1865,53 @@ static void ntfs_put_super(struct super_block *sb)
ntfs_commit_inode(vol->mft_ino);
write_inode_now(vol->mft_ino, 1);
+ /*
+ * If a read-write mount, persist the error state in the volume flags:
+ * mark the volume clean if no volume errors have occurred, and make
+ * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
+ * next mount.
+ */
+ if (!sb_rdonly(sb)) {
+ if (ntfs_sync_volume_dirty_state(vol)) {
+ ntfs_warning(sb,
+ "Failed to sync dirty bit in volume information flags. Run chkdsk.");
+ } else if (NVolErrors(vol)) {
+ /*
+ * The dirty bit is on disk now; only warn when the
+ * sync actually succeeded, or this message would
+ * contradict the one above.
+ */
+ ntfs_warning(sb,
+ "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
+ }
+ /*
+ * Commits the updated volume flags if they were written.
+ * The mft mirror must still be around for this: the
+ * $Volume record (mft record number 3, below
+ * vol->mftmirr_size) is mirrored by write_mft_record()
+ * through ntfs_sync_mft_mirror(), which fails with -EIO
+ * and leaves the mirror stale once vol->mftmirr_ino is
+ * gone, so the mirror inode is only released after this
+ * commit.
+ */
+ ntfs_commit_inode(vol->vol_ino);
+ }
+
+ /*
+ * Release $Volume while the mft inode is still available: if the
+ * commit above failed before it could clear the dirty flag,
+ * ntfs_evict_big_inode() commits the inode again on its way out,
+ * and __ntfs_write_inode() needs vol->mft_ino to look up the
+ * runlist of the record to write.
+ */
+ iput(vol->vol_ino);
+ vol->vol_ino = NULL;
+
+ if (vol->mftmirr_ino) {
+ iput(vol->mftmirr_ino);
+ vol->mftmirr_ino = NULL;
+ }
+
iput(vol->mft_ino);
vol->mft_ino = NULL;
blkdev_issue_flush(sb->s_bdev);
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 5/7] ntfs: do not clear the volume dirty bit during sync
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (3 preceding siblings ...)
2026-09-15 8:32 ` [PATCH v13 4/7] ntfs: persist the dirty state after the final put_super() commits Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 6/7] ntfs: check the dirty-state commit on remount and unmount Hongling Zeng
` (4 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, Baolin Liu, stable
ntfs_sync_fs() clears VOLUME_IS_DIRTY while the volume is still mounted
read-write, so a sync running concurrently with an in-flight metadata
modification can clear and persist a bit that was just set: the
modification then lands on a volume that is clean on disk, and a crash
does not run chkdsk.
Stop clearing the bit in ntfs_sync_fs() and leave the clearing to the
remount-to-read-only path, which the VFS reaches only after
sb_prepare_remount_readonly() has drained in-flight writers, and to
ntfs_put_super(), which runs after evict_inodes() on a quiesced
filesystem. A mounted read-write volume now keeps the dirty bit until
it is dismounted cleanly, which matches the NTFS semantics; the cost is
a needless chkdsk if the machine crashes between a sync and the unmount.
A recorded error state is still persisted right away when the
filesystem is synced: with NVolErrors() set,
ntfs_sync_volume_dirty_state() can only set the bit, never clear it,
so it cannot lose a modification the way the old unconditional call
did. This keeps the error report from being lost to a crash on a
volume that has seen no modification; an error recorded after the
last sync is still only persisted at the next quiescent transition.
sync_blockdev() and blkdev_issue_flush() are both called and the first
error is returned, so a writeback failure neither hides a flush failure
nor skips it.
A RWF_NOWAIT write still blocks in the marking when the volume looks
clean, as it already did on the base; a non-blocking marking is
follow-up work.
Reported-by: Baolin Liu <liubaolin@kylinos.cn>
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/file.c | 7 ++++---
fs/ntfs/super.c | 31 ++++++++++++++++++++++++-------
2 files changed, 28 insertions(+), 10 deletions(-)
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index cfc7b36b7dff..99a2c7a5cf81 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -621,9 +621,10 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
/*
* The volume must be marked dirty before the modification is made,
- * without an unlocked check of the in-memory flag: ntfs_sync_fs()
- * can clear the bit concurrently and the modification would then
- * land on a volume that is clean on disk.
+ * without an unlocked check of the in-memory flag: the dirty bit
+ * is only cleared at the quiescent transitions, under the same
+ * $Volume mrec_lock this call takes, so an unlocked skip could
+ * lose the set to one of them.
*/
ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 3574c224fe28..463050ae1b0e 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -486,8 +486,9 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
* This is the single point that persists the in-memory error state to disk.
* The runtime error paths only record NVolErrors() because they run under a
* variety of ntfs locks the dirty-bit write cannot be taken under (runlist
- * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first
- * ntfs_sync_fs(), a remount, or the unmount then persists the flag here.
+ * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); a sync of a
+ * volume with recorded errors, a remount to read-only, or the unmount,
+ * then persists the flag here.
*
* A hibernated volume is not written from these persistence paths:
* resuming Windows from a modified image corrupts it, so the dirty bit
@@ -1955,7 +1956,7 @@ static void ntfs_shutdown(struct super_block *sb)
static int ntfs_sync_fs(struct super_block *sb, int wait)
{
struct ntfs_volume *vol = NTFS_SB(sb);
- int err = 0;
+ int ret, err = 0;
if (NVolShutdown(vol))
return -EIO;
@@ -1963,14 +1964,30 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
if (!wait)
return 0;
- /* If there are some dirty buffers in the bdev inode */
- if (ntfs_sync_volume_dirty_state(vol)) {
+ /*
+ * The volume dirty bit is deliberately not cleared here: a sync
+ * running concurrently with an in-flight modification could clear
+ * and persist a bit that was just set, leaving the modification
+ * on a volume that is clean on disk. The bit is only cleared at
+ * the quiescent state transitions, remounting read-only and clean
+ * unmount. A recorded error state, however, is persisted right
+ * away so that it is not lost to a crash on a volume that has
+ * seen no modification; with NVolErrors() set this can only set
+ * the bit, never clear it.
+ */
+ if (NVolErrors(vol) &&
+ ntfs_sync_volume_dirty_state(vol)) {
ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk.");
err = -EIO;
}
sync_inodes_sb(sb);
- sync_blockdev(sb->s_bdev);
- blkdev_issue_flush(sb->s_bdev);
+ ret = sync_blockdev(sb->s_bdev);
+ if (ret && !err)
+ err = ret;
+
+ ret = blkdev_issue_flush(sb->s_bdev);
+ if (ret && !err)
+ err = ret;
return err;
}
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 6/7] ntfs: check the dirty-state commit on remount and unmount
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (4 preceding siblings ...)
2026-09-15 8:32 ` [PATCH v13 5/7] ntfs: do not clear the volume dirty bit during sync Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 7/7] ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE Hongling Zeng
` (3 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, Baolin Liu, stable
The remount-to-read-only path commits the updated volume flags with
ntfs_commit_inode(), a void wrapper around __ntfs_write_inode(), and
ignores the blkdev_issue_flush() return value, so a failed commit or
flush is reported as success. Once the remount has succeeded no
persistence point is ever reached again: ntfs_put_super() skips
read-only superblocks and the VFS never syncs one, so fail the
remount unless the commit and the flush succeed. The superblock
then stays read-write and ntfs_put_super() retries the persistence
at unmount. The errors=remount-ro downgrade does not go through
ntfs_reconfigure() and is unchanged.
A zero-return commit is not trusted blindly: write_mft_record()
redirties the record on allocation failure and reports success, so
the $Volume inode is required to be clean afterwards.
ntfs_put_super() discards the same commit error. Call
__ntfs_write_inode() there with the same dirty re-check and warn on
failure, as put_super() cannot return an error. The commit is
skipped when the dirty-state sync itself failed, as that could write
back an inconsistent flag state; a record left dirty by an earlier
update is still committed at evict time.
NVolErrors() is deliberately not used to detect the failure: it is
sticky for the lifetime of the mount, so it cannot distinguish a
fresh commit failure from errors recorded before the remount.
Hibernated volumes: ntfs_sync_volume_dirty_state() is a no-op for
them and never dirties the $Volume inode on such a mount, since the
on-disk flags are already dirty and ntfs_set_volume_flags() has
nothing to change. The commit only runs if something dirtied the
inode independently, as before this patch; mounting hibernated
volumes read-only removes even that.
Reported-by: Baolin Liu <liubaolin@kylinos.cn>
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/super.c | 76 +++++++++++++++++++++++++++++++++++--------------
1 file changed, 54 insertions(+), 22 deletions(-)
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 463050ae1b0e..0228d7429596 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -268,6 +268,7 @@ static int ntfs_reconfigure(struct fs_context *fc)
{
struct super_block *sb = fc->root->d_sb;
struct ntfs_volume *vol = NTFS_SB(sb);
+ int err;
ntfs_debug("Entering with remount");
@@ -324,14 +325,39 @@ static int ntfs_reconfigure(struct fs_context *fc)
* and ntfs_put_super() skips them, so the only remaining
* write would be the evict-time commit at unmount, which
* a crash never reaches. An error recorded only after
- * the remount is still never persisted.
+ * the remount is still never persisted; a failed commit
+ * or flush fails the remount, leaving the superblock
+ * read-write so ntfs_put_super() retries at unmount.
*/
- if (ntfs_sync_volume_dirty_state(vol)) {
+ err = ntfs_sync_volume_dirty_state(vol);
+ if (err) {
ntfs_warning(sb,
"Failed to update dirty bit in volume information flags. Run chkdsk.");
- } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
- ntfs_commit_inode(vol->vol_ino);
- blkdev_issue_flush(sb->s_bdev);
+ return err;
+ }
+ if (NInoDirty(NTFS_I(vol->vol_ino))) {
+ /* ntfs_commit_inode() would discard the error. */
+ err = __ntfs_write_inode(vol->vol_ino, 1);
+ if (err) {
+ ntfs_warning(sb,
+ "Failed to commit volume information flags. Run chkdsk.");
+ return err;
+ }
+ /*
+ * write_mft_record() redirties the record on
+ * -ENOMEM and still reports success.
+ */
+ if (NInoDirty(NTFS_I(vol->vol_ino))) {
+ ntfs_warning(sb,
+ "Volume information flags remain dirty after commit. Run chkdsk.");
+ return -EIO;
+ }
+ err = blkdev_issue_flush(sb->s_bdev);
+ if (err) {
+ ntfs_warning(sb,
+ "Failed to flush volume information flags. Run chkdsk.");
+ return err;
+ }
}
}
@@ -1876,26 +1902,32 @@ static void ntfs_put_super(struct super_block *sb)
if (ntfs_sync_volume_dirty_state(vol)) {
ntfs_warning(sb,
"Failed to sync dirty bit in volume information flags. Run chkdsk.");
- } else if (NVolErrors(vol)) {
+ } else {
/*
- * The dirty bit is on disk now; only warn when the
- * sync actually succeeded, or this message would
- * contradict the one above.
+ * __ntfs_write_inode(), not the void
+ * ntfs_commit_inode() wrapper: the error can only
+ * be warned about here. The mirror inode is only
+ * released below: writing the $Volume record (mft
+ * record number 3, below vol->mftmirr_size) mirrors
+ * it through ntfs_sync_mft_mirror(), which fails
+ * with -EIO once vol->mftmirr_ino is gone.
*/
- ntfs_warning(sb,
- "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
+ if (__ntfs_write_inode(vol->vol_ino, 1)) {
+ ntfs_warning(sb,
+ "Failed to commit volume information flags. Run chkdsk.");
+ } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
+ ntfs_warning(sb,
+ "Volume information flags remain dirty after commit. Run chkdsk.");
+ } else if (NVolErrors(vol)) {
+ /*
+ * Only warn once the commit has succeeded,
+ * or this could contradict a failure
+ * reported above.
+ */
+ ntfs_warning(sb,
+ "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
+ }
}
- /*
- * Commits the updated volume flags if they were written.
- * The mft mirror must still be around for this: the
- * $Volume record (mft record number 3, below
- * vol->mftmirr_size) is mirrored by write_mft_record()
- * through ntfs_sync_mft_mirror(), which fails with -EIO
- * and leaves the mirror stale once vol->mftmirr_ino is
- * gone, so the mirror inode is only released after this
- * commit.
- */
- ntfs_commit_inode(vol->vol_ino);
}
/*
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v13 7/7] ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (5 preceding siblings ...)
2026-09-15 8:32 ` [PATCH v13 6/7] ntfs: check the dirty-state commit on remount and unmount Hongling Zeng
@ 2026-09-15 8:32 ` Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-16 0:42 ` [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hyunchul Lee
` (2 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Hongling Zeng @ 2026-09-15 8:32 UTC (permalink / raw)
To: linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Hongling Zeng, stable
ntfs_reconfigure() currently ignores sync_filesystem() errors, allowing
a regular read-only remount to succeed even when dirty data was not
synced. Check the error and fail regular remounts, leaving the
superblock read-write so ntfs_put_super() can retry at unmount.
SB_FORCE does not wait for writers already in progress, so it must not
clear the on-disk dirty bit. Warn and continue on sync errors for
SB_FORCE. A forced remount updates the dirty state only when errors
have been recorded, and never clears the dirty bit. Skip the commit if
that state update fails, since the in-memory flags may be inconsistent.
Reported-by: Hyunchul Lee <hyc.lee@gmail.com>
Cc: stable@vger.kernel.org
Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
---
fs/ntfs/super.c | 27 +++++++++++++++++++++------
1 file changed, 21 insertions(+), 6 deletions(-)
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 0228d7429596..82b5bf28b6ab 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -272,7 +272,13 @@ static int ntfs_reconfigure(struct fs_context *fc)
ntfs_debug("Entering with remount");
- sync_filesystem(sb);
+ err = sync_filesystem(sb);
+ if (err) {
+ ntfs_warning(sb, "Failed to sync the filesystem.");
+ /* A forced remount must still turn the superblock read-only. */
+ if (!(fc->sb_flags & SB_FORCE))
+ return err;
+ }
/*
* For the read-write compiled driver, if we are remounting read-write,
@@ -329,11 +335,20 @@ static int ntfs_reconfigure(struct fs_context *fc)
* or flush fails the remount, leaving the superblock
* read-write so ntfs_put_super() retries at unmount.
*/
- err = ntfs_sync_volume_dirty_state(vol);
- if (err) {
- ntfs_warning(sb,
- "Failed to update dirty bit in volume information flags. Run chkdsk.");
- return err;
+ /*
+ * A forced remount does not drain writers in progress,
+ * so one may still be modifying metadata when the flags
+ * are committed: never clear the dirty bit then; if
+ * errors have been recorded, the update preserves or
+ * sets it; otherwise, skip the update entirely.
+ */
+ if (!(fc->sb_flags & SB_FORCE) || NVolErrors(vol)) {
+ err = ntfs_sync_volume_dirty_state(vol);
+ if (err) {
+ ntfs_warning(sb,
+ "Failed to update dirty bit in volume information flags. Run chkdsk.");
+ return err;
+ }
}
if (NInoDirty(NTFS_I(vol->vol_ino))) {
/* ntfs_commit_inode() would discard the error. */
--
2.25.1
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (6 preceding siblings ...)
2026-09-15 8:32 ` [PATCH v13 7/7] ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE Hongling Zeng
@ 2026-09-16 0:42 ` Hyunchul Lee
2026-09-16 21:36 ` liubaolin
2026-09-16 23:36 ` Namjae Jeon
9 siblings, 0 replies; 19+ messages in thread
From: Hyunchul Lee @ 2026-09-16 0:42 UTC (permalink / raw)
To: Hongling Zeng; +Cc: linkinjeon, ntfs, linux-kernel, zhongling0719
This patch set looks good to me.
For whole series:
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
2026년 9월 15일 (화) 오후 5:33, Hongling Zeng <zenghongling@kylinos.cn>님이 작성:
>
> Hi all,
>
> The fs/ntfs runtime metadata-corruption paths only record the in-memory
> NVolErrors() flag, and the dirty-bit persistence used to race with
> ntfs_sync_fs(): a volume could end up with a clean on-disk dirty flag
> despite modification or recorded corruption, so chkdsk would not run on
> the next mount. This series fixes that.
>
> 1/6 makes the volume flag read-modify-write atomic under the
> $Volume mrec_lock;
> 2/6 marks the volume dirty unconditionally on metadata changes,
> dropping the racy caller-side checks in file.c and namei.c;
> 3/6 derives the on-disk dirty bit from the recorded error state at
> the persistence points (sync_fs, remount-ro, put_super) and never
> writes a hibernated volume;
> 4/6 persists the dirty state after the final put_super() commits so
> late errors cannot unmount clean;
> 5/6 stops ntfs_sync_fs() from clearing VOLUME_IS_DIRTY: the clearing
> moves to the quiescent transitions, a recorded error state is
> still persisted at sync time, and sync now reports writeback and
> flush errors instead of discarding them;
> 6/6 checks the dirty-state commit on remount and unmount.
> 7/7 ntfs: fail remount on sync errors and keep the dirty bit on
> SB_FORCE
>
> Changes in this revision, from the review:
>
> - 5/6: with the clearing gone from the sync path, a recorded error
> state is persisted without ever clearing the bit, and
> sync_blockdev() and blkdev_issue_flush() are both called with the
> first error returned.
>
> - The IOCB_NOWAIT behavior and the per-operation $Volume mrec_lock
> acquisition are outside the scope of this series. The series keeps
> the unconditional ntfs_set_volume_flags() call to preserve the
> ordering that marks the volume dirty before the metadata
> modification; a RWF_NOWAIT write still blocks in the marking when
> the volume looks clean, as it already did on the base. The
> non-blocking and contended-lock handling (mutex_trylock, GFP_NOWAIT)
> will be addressed in a separate follow-up patch.
>
> Hongling Zeng (4):
> ntfs: fix volume flag update races
> ntfs: set the volume dirty bit unconditionally on metadata changes
> ntfs: sync the volume dirty bit with the recorded error state
> ntfs: persist the dirty state after the final put_super() commits
>
> fs/ntfs/file.c | 20 ++---
> fs/ntfs/namei.c | 24 ++----
> fs/ntfs/ntfs.h | 1 -
> fs/ntfs/super.c | 191 ++++++++++++++++++++++++++++++++++++-----------
> fs/ntfs/volume.h | 4 +
> 5 files changed, 171 insertions(+), 69 deletions(-)
>
> --
> 2.25.1
>
--
Thanks,
Hyunchul
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 1/7] ntfs: fix volume flag update races
2026-09-15 8:32 ` [PATCH v13 1/7] ntfs: fix volume flag update races Hongling Zeng
@ 2026-09-16 20:23 ` liubaolin
0 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 20:23 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> ntfs_set_volume_flags() and ntfs_clear_volume_flags() both read
> vol->vol_flags outside any lock to compute the new value before handing
> it to ntfs_write_volume_flags(), which only takes ni->mrec_lock around
> the actual write. The read-modify-write is therefore not atomic, and two
> concurrent callers can lose an update: ntfs_sync_fs() may derive a
> "clean" value from vol->vol_flags while a writer concurrently records an
> error and sets VOLUME_IS_DIRTY; the locked write then silently
> overwrites the freshly-set dirty bit. The on-disk volume looks clean
> despite the recorded errors, so chkdsk will not run on the next mount
> and corrupted metadata can persist.
>
> Fix by moving the read-modify-write inside the mrec_lock: pass the bits
> to set and to clear separately, and combine them with the current flag
> state under the lock inside ntfs_write_volume_flags(). The set/clear
> helpers pass only the bits to modify, not the complete flag state. The
> bit manipulation is done on CPU-endian values, and the result is
> converted back to little-endian before storing it. The wrappers keep
> their signatures so callers are unchanged.
>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/super.c | 63 +++++++++++++++++++++++++++++++++++--------------
> 1 file changed, 45 insertions(+), 18 deletions(-)
>
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index f4a73e45773d..6ba19986a598 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -353,31 +353,45 @@ void ntfs_handle_error(struct super_block *sb)
> }
>
> /*
> - * ntfs_write_volume_flags - write new flags to the volume information flags
> + * ntfs_write_volume_flags - apply flag changes to the volume information flags
> * @vol: ntfs volume on which to modify the flags
> - * @flags: new flags value for the volume information flags
> + * @set_bits: bits to set in the volume information flags
> + * @clear_bits: bits to clear in the volume information flags
> *
> * Internal function. You probably want to use ntfs_{set,clear}_volume_flags()
> * instead (see below).
> *
> - * Replace the volume information flags on the volume @vol with the value
> - * supplied in @flags. Note, this overwrites the volume information flags, so
> - * make sure to combine the flags you want to modify with the old flags and use
> - * the result when calling ntfs_write_volume_flags().
> + * Combine @set_bits and @clear_bits with the current in-memory flag state and
> + * write the result back. The set/clear helpers pass only the bits to modify,
> + * not the complete flag state. The read-modify-write happens under
> + * ni->mrec_lock so that concurrent set/clear operations cannot lose updates.
> + * All bit manipulation is done on CPU-endian values, and the result is
> + * converted back to little-endian before storing it.
> *
> * Return 0 on success and -errno on error.
> */
> -static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags)
> +static int ntfs_write_volume_flags(struct ntfs_volume *vol,
> + const __le16 set_bits, const __le16 clear_bits,
> + const bool skip_if_errors)
> {
> struct ntfs_inode *ni = NTFS_I(vol->vol_ino);
> struct volume_information *vi;
> struct ntfs_attr_search_ctx *ctx;
> + u16 flags;
> int err;
>
> - ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
> - le16_to_cpu(vol->vol_flags), le16_to_cpu(flags));
> mutex_lock(&ni->mrec_lock);
> - if (vol->vol_flags == flags)
> +
> + if (skip_if_errors && NVolErrors(vol))
> + goto done;
> +
> + flags = le16_to_cpu(vol->vol_flags);
> + flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK);
> + flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK));
> + ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
> + le16_to_cpu(vol->vol_flags), flags);
> +
> + if (le16_to_cpu(vol->vol_flags) == flags)
> goto done;
>
> ctx = ntfs_attr_get_search_ctx(ni, NULL);
> @@ -393,7 +407,7 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags)
>
> vi = (struct volume_information *)((u8 *)ctx->attr +
> le16_to_cpu(ctx->attr->data.resident.value_offset));
> - vol->vol_flags = vi->flags = flags;
> + vol->vol_flags = vi->flags = cpu_to_le16(flags);
> mark_mft_record_dirty(ctx->ntfs_ino);
> ntfs_attr_put_search_ctx(ctx);
> done:
> @@ -414,13 +428,14 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol, const __le16 flags)
> * @flags: flags to set on the volume
> *
> * Set the bits in @flags in the volume information flags on the volume @vol.
> + * The bits are combined with the current flag state under the lock in
> + * ntfs_write_volume_flags(), so concurrent updates are not lost.
> *
> * Return 0 on success and -errno on error.
> */
> int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
> {
> - flags &= VOLUME_FLAGS_MASK;
> - return ntfs_write_volume_flags(vol, vol->vol_flags | flags);
> + return ntfs_write_volume_flags(vol, flags, 0, false);
> }
>
> /*
> @@ -429,14 +444,27 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
> * @flags: flags to clear on the volume
> *
> * Clear the bits in @flags in the volume information flags on the volume @vol.
> + * The bits are combined with the current flag state under the lock in
> + * ntfs_write_volume_flags(), so concurrent updates are not lost.
> *
> * Return 0 on success and -errno on error.
> */
> int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags)
> {
> - flags &= VOLUME_FLAGS_MASK;
> - flags = vol->vol_flags & cpu_to_le16(~le16_to_cpu(flags));
> - return ntfs_write_volume_flags(vol, flags);
> + return ntfs_write_volume_flags(vol, 0, flags, false);
> +}
> +
> +/*
> + * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist
> + * @vol: ntfs volume whose dirty bit should be cleared
> + *
> + * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so
> + * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been
> + * recorded.
> + */
> +static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol)
> +{
> + return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true);
> }
>
> int ntfs_write_volume_label(struct ntfs_volume *vol, char *label)
> @@ -1858,8 +1886,7 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
> return 0;
>
> /* If there are some dirty buffers in the bdev inode */
> - if (!NVolErrors(vol) &&
> - ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY)) {
> + if (ntfs_clear_volume_dirty_if_no_errors(vol)) {
> ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> err = -EIO;
> }
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 2/7] ntfs: set the volume dirty bit unconditionally on metadata changes
2026-09-15 8:32 ` [PATCH v13 2/7] ntfs: set the volume dirty bit unconditionally on metadata changes Hongling Zeng
@ 2026-09-16 20:23 ` liubaolin
0 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 20:23 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Baolin Liu, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> The callers in file.c and namei.c skip ntfs_set_volume_flags() when
> the in-memory vol_flags already show VOLUME_IS_DIRTY, but that check
> runs without any lock: if it observes the bit set and ntfs_sync_fs()
> clears it under the mrec_lock before the caller's metadata update
> completes, the set is skipped and the volume can end up clean on disk
> despite the modification, so chkdsk will not run on the next mount.
>
> Drop the caller-side checks and call ntfs_set_volume_flags()
> unconditionally: ntfs_write_volume_flags() already skips the write
> under the mrec_lock when the combined value is unchanged. That
> unconditional call costs one mrec_lock acquisition per metadata
> operation even in the already-dirty steady state; it cannot be
> avoided, because deciding to skip the call without the lock is itself
> what allows a concurrent ntfs_sync_fs() clear to lose the set.
>
> The IOCB_NOWAIT path in ntfs_file_write_iter() goes through the same
> sleeping call: a RWF_NOWAIT write can block in the marking, as it
> already could before this change whenever the volume appeared clean.
> Giving that path a non-blocking variant is left as follow-up work.
> The callers keep the pre-existing behavior of proceeding when the
> marking fails, so the dirty bit remains best-effort.
>
> This closes the variant where the set is skipped outright. A clear
> for a concurrent, error-free sync can still land between the set and
> the end of the metadata operation; that mark-at-start lifecycle is
> pre-existing and is not changed by this patch.
>
> Reported-by: Baolin Liu <liubaolin@kylinos.cn>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/file.c | 20 +++++++++++---------
> fs/ntfs/namei.c | 24 ++++++++----------------
> 2 files changed, 19 insertions(+), 25 deletions(-)
>
> diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
> index 007d1614b9ac..cfc7b36b7dff 100644
> --- a/fs/ntfs/file.c
> +++ b/fs/ntfs/file.c
> @@ -325,8 +325,7 @@ int ntfs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
> goto out;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> if (ia_valid & ATTR_SIZE) {
> err = ntfs_setattr_size(vi, attr);
> @@ -620,8 +619,13 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
> goto out_lock;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + /*
> + * The volume must be marked dirty before the modification is made,
> + * without an unlocked check of the in-memory flag: ntfs_sync_fs()
> + * can clear the bit concurrently and the modification would then
> + * land on a volume that is clean on disk.
> + */
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> pos = iocb->ki_pos;
> count = ret;
> @@ -1153,11 +1157,9 @@ static long ntfs_fallocate(struct file *file, int mode, loff_t offset, loff_t le
> return err;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY)) {
> - err = ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> - if (err)
> - return err;
> - }
> + err = ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + if (err)
> + return err;
>
> old_size = i_size_read(vi);
>
> diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
> index fdf52fac4329..3e0adb9a0ea4 100644
> --- a/fs/ntfs/namei.c
> +++ b/fs/ntfs/namei.c
> @@ -757,8 +757,7 @@ static int ntfs_create(struct mnt_idmap *idmap, struct inode *dir,
> return err;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> ni = __ntfs_create(idmap, dir, uname, uname_len, S_IFREG | mode, 0, NULL, 0);
> kmem_cache_free(ntfs_name_cache, uname);
> @@ -1032,8 +1031,7 @@ static int ntfs_unlink(struct inode *dir, struct dentry *dentry)
> return err;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> err = ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true);
> if (err)
> @@ -1076,8 +1074,7 @@ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir,
> return ERR_PTR(err);
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> ni = __ntfs_create(idmap, dir, uname, uname_len, mode, 0, NULL, 0);
> kmem_cache_free(ntfs_name_cache, uname);
> @@ -1118,8 +1115,7 @@ static int ntfs_rmdir(struct inode *dir, struct dentry *dentry)
> return err;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> err = ntfs_delete(ni, NTFS_I(dir), uname, uname_len, true);
> if (err)
> @@ -1305,8 +1301,7 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
> new_dir_first = is_subdir(new_dentry->d_parent,
> old_dentry->d_parent);
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> mutex_lock_nested(&old_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
> if (new_ni)
> @@ -1429,8 +1424,7 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
> goto out;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> ni = __ntfs_create(idmap, dir, usrc, usrc_len, S_IFLNK | 0777, 0,
> symname, symlen);
> @@ -1474,8 +1468,7 @@ static int ntfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
> return err;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> switch (mode & S_IFMT) {
> case S_IFCHR:
> @@ -1521,8 +1514,7 @@ static int ntfs_link(struct dentry *old_dentry, struct inode *dir,
> return -ENOMEM;
> }
>
> - if (!(vol->vol_flags & VOLUME_IS_DIRTY))
> - ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
> + ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> ihold(vi);
> mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state
2026-09-15 8:32 ` [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state Hongling Zeng
@ 2026-09-16 20:23 ` liubaolin
2026-09-16 21:17 ` liubaolin
1 sibling, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 20:23 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> The runtime metadata-corruption paths in fs/ntfs only record the
> in-memory NVolErrors() flag; whether VOLUME_IS_DIRTY ever reaches disk
> depends on ntfs_set_volume_flags() being called by some other path,
> which for most error sites never happens. A volume can therefore
> unmount with a clean on-disk flag despite recorded corruption, and
> chkdsk will not run on the next mount.
>
> Persisting the dirty bit from the error paths themselves does not work:
> they run under a wide variety of ntfs locks, and the dirty-bit write
> takes the $Volume mrec_lock and maps the $Volume mft record, which on
> an $MFT page-cache miss takes the $MFT runlist lock for writing. That
> is enough to self-deadlock or form ABBA cycles from several of them:
> the $MFT extend undo paths hold the $MFT runlist lock and then take
> vol->lcnbmp_lock inside ntfs_cluster_free(); the cluster allocation and
> free rollback paths hold vol->lcnbmp_lock; and the whole mft record
> allocation tree is reachable from ntfs_write_volume_label()'s
> attribute-list maintenance while it holds the $Volume mrec_lock itself.
>
> Instead, make the persistence a property of the sync paths, which run
> without ntfs locks held. The new ntfs_sync_volume_dirty_state() sets
> VOLUME_IS_DIRTY when NVolErrors() is recorded and clears it otherwise,
> evaluating the error flag under the $Volume mrec_lock. It is called
> from ntfs_sync_fs(), from the remount-to-read-only path of
> ntfs_reconfigure(), and from ntfs_put_super(), which previously
> evaluated NVolErrors() outside the lock before clearing the dirty bit
> unconditionally, and which now also persists the dirty bit for volumes
> with recorded errors so they unmount with chkdsk scheduled. The
> ntfs_clear_volume_flags() wrapper, whose last callers this patch
> replaces, has no users left and is removed.
>
> The guarantee this provides is eventual, not instantaneous: the error
> paths record NVolErrors() with a lock-free set_bit(), so a persistence
> point that evaluates the flag just before an error is recorded can
> still leave the on-disk bit clean until the next one. This is sound
> because NVolErrors() is sticky for the lifetime of the mount and every
> persistence point re-derives the on-disk bit from it; the last one,
> ntfs_put_super(), runs after evict_inodes() on a quiesced filesystem,
> so a volume that is read-write at unmount time cannot unmount clean.
> A volume that is already read-only when the error is recorded
> (errors=remount-ro flips the superblock on the first error, as does an
> earlier remount-ro) has no persistence point left and keeps whatever
> on-disk bit it had; that behaviour is unchanged. The residual window
> is a crash between the error and the next persistence point.
>
> The persistence paths never write a hibernated volume: resuming Windows
> from a modified image corrupts it. Record the mount-time hibernation
> verdict in the new NV_Hibernated volume flag and make
> ntfs_sync_volume_dirty_state() a no-op while it is set, so the dirty
> bit is left exactly as it is on disk and only the in-memory error
> state is kept. Without this, an rw mount of a hibernated volume with
> the default errors=continue would gain a filesystem-internal write on
> the first sync, remount or unmount. Other writes to such a mount,
> like the mount-time logfile emptying, are pre-existing and unchanged.
>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/ntfs.h | 1 -
> fs/ntfs/super.c | 129 ++++++++++++++++++++++++++++++++---------------
> fs/ntfs/volume.h | 4 ++
> 3 files changed, 93 insertions(+), 41 deletions(-)
>
> diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h
> index 45f77848a9cf..a5cd5493c501 100644
> --- a/fs/ntfs/ntfs.h
> +++ b/fs/ntfs/ntfs.h
> @@ -219,7 +219,6 @@ struct option_t {
> };
> extern const struct option_t on_errors_arr[];
> int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags);
> -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags);
> int ntfs_write_volume_label(struct ntfs_volume *vol, char *label);
>
> /* From fs/ntfs/mst.c */
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index 6ba19986a598..733565953302 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -262,6 +262,8 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
> return 0;
> }
>
> +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol);
> +
> static int ntfs_reconfigure(struct fs_context *fc)
> {
> struct super_block *sb = fc->root->d_sb;
> @@ -312,10 +314,24 @@ static int ntfs_reconfigure(struct fs_context *fc)
> }
> } else if (!sb_rdonly(sb) && (fc->sb_flags & SB_RDONLY)) {
> /* Remounting read-only. */
> - if (!NVolErrors(vol)) {
> - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY))
> - ntfs_warning(sb,
> - "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> + /*
> + * With errors recorded the dirty bit is set rather than
> + * cleared, and it is committed right away: the VFS does
> + * not sync the filesystem during a remount, and once the
> + * remount succeeds no further persistence point exists -
> + * ntfs_sync_fs() is only ever invoked for read-write
> + * superblocks (all its VFS callers skip read-only ones)
> + * and ntfs_put_super() skips them, so the only remaining
> + * write would be the evict-time commit at unmount, which
> + * a crash never reaches. An error recorded only after
> + * the remount is still never persisted.
> + */
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb,
> + "Failed to update dirty bit in volume information flags. Run chkdsk.");
> + } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
> + ntfs_commit_inode(vol->vol_ino);
> + blkdev_issue_flush(sb->s_bdev);
> }
> }
>
> @@ -357,9 +373,10 @@ void ntfs_handle_error(struct super_block *sb)
> * @vol: ntfs volume on which to modify the flags
> * @set_bits: bits to set in the volume information flags
> * @clear_bits: bits to clear in the volume information flags
> + * @dirty_if_errors: force VOLUME_IS_DIRTY on when NVolErrors() is set
> *
> * Internal function. You probably want to use ntfs_{set,clear}_volume_flags()
> - * instead (see below).
> + * or ntfs_sync_volume_dirty_state() instead (see below).
> *
> * Combine @set_bits and @clear_bits with the current in-memory flag state and
> * write the result back. The set/clear helpers pass only the bits to modify,
> @@ -368,11 +385,18 @@ void ntfs_handle_error(struct super_block *sb)
> * All bit manipulation is done on CPU-endian values, and the result is
> * converted back to little-endian before storing it.
> *
> + * When @dirty_if_errors is true and errors have been recorded on @vol,
> + * VOLUME_IS_DIRTY is forced on after the requested changes. NVolErrors() is
> + * evaluated under the same mrec_lock, which orders this against other
> + * locked flag updates; the runtime error paths themselves record the flag
> + * lock-free, so see ntfs_sync_volume_dirty_state() for the guarantee this
> + * provides against them.
> + *
> * Return 0 on success and -errno on error.
> */
> static int ntfs_write_volume_flags(struct ntfs_volume *vol,
> const __le16 set_bits, const __le16 clear_bits,
> - const bool skip_if_errors)
> + const bool dirty_if_errors)
> {
> struct ntfs_inode *ni = NTFS_I(vol->vol_ino);
> struct volume_information *vi;
> @@ -382,12 +406,11 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol,
>
> mutex_lock(&ni->mrec_lock);
>
> - if (skip_if_errors && NVolErrors(vol))
> - goto done;
> -
> flags = le16_to_cpu(vol->vol_flags);
> flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK);
> flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK));
> + if (dirty_if_errors && NVolErrors(vol))
> + flags |= le16_to_cpu(VOLUME_IS_DIRTY);
> ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
> le16_to_cpu(vol->vol_flags), flags);
>
> @@ -439,31 +462,43 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
> }
>
> /*
> - * ntfs_clear_volume_flags - clear bits in the volume information flags
> - * @vol: ntfs volume on which to modify the flags
> - * @flags: flags to clear on the volume
> + * ntfs_sync_volume_dirty_state - persist the dirty bit per the error state
> + * @vol: ntfs volume whose dirty bit to persist
> *
> - * Clear the bits in @flags in the volume information flags on the volume @vol.
> - * The bits are combined with the current flag state under the lock in
> - * ntfs_write_volume_flags(), so concurrent updates are not lost.
> + * Set VOLUME_IS_DIRTY if errors have been recorded on @vol and clear it
> + * otherwise, under the $Volume mrec_lock.
> *
> - * Return 0 on success and -errno on error.
> - */
> -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags)
> -{
> - return ntfs_write_volume_flags(vol, 0, flags, false);
> -}
> -
> -/*
> - * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist
> - * @vol: ntfs volume whose dirty bit should be cleared
> + * The guarantee this provides is eventual, not instantaneous: the runtime
> + * error paths record NVolErrors() with a lock-free set_bit(), so a
> + * persistence point that evaluates the flag just before an error is
> + * recorded can still leave the on-disk bit clean. This is sound because
> + * NVolErrors() is sticky (nothing clears it for the lifetime of the mount)
> + * and every persistence point re-derives the on-disk bit from it; the
> + * last one, ntfs_put_super(), runs after evict_inodes() on a quiesced
> + * filesystem, so a volume that is read-write at unmount time cannot
> + * unmount clean. A volume that is already read-only when the error is
> + * recorded (errors=remount-ro flips the superblock on the first error,
> + * as does an earlier remount-ro) has no persistence point left and
> + * keeps whatever on-disk bit it had; that behaviour is unchanged. The
> + * residual window is a crash between the error and the next
> + * persistence point.
> + *
> + * This is the single point that persists the in-memory error state to disk.
> + * The runtime error paths only record NVolErrors() because they run under a
> + * variety of ntfs locks the dirty-bit write cannot be taken under (runlist
> + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first
> + * ntfs_sync_fs(), a remount, or the unmount then persists the flag here.
> + *
> + * A hibernated volume is not written from these persistence paths:
> + * resuming Windows from a modified image corrupts it, so the dirty bit
> + * is left as it is on disk and only the in-memory error state is kept.
> *
> - * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so
> - * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been
> - * recorded.
> + * Return 0 on success and -errno on error.
> */
> -static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol)
> +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol)
> {
> + if (NVolHibernated(vol))
> + return 0;
> return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true);
> }
>
> @@ -1615,6 +1650,11 @@ static bool load_system_files(struct ntfs_volume *vol)
> ntfs_error(sb, "%s. Mounting read-only%s", es1, es2);
> }
> NVolSetErrors(vol);
> + /*
> + * Remember it for the lifetime of the mount: see
> + * ntfs_sync_volume_dirty_state().
> + */
> + NVolSetHibernated(vol);
> }
>
> /* If (still) a read-write mount, empty the logfile. */
> @@ -1772,22 +1812,31 @@ static void ntfs_put_super(struct super_block *sb)
> ntfs_commit_inode(vol->mft_ino);
>
> /*
> - * If a read-write mount and no volume errors have occurred, mark the
> - * volume clean. Also, re-commit all affected inodes.
> + * If a read-write mount, persist the error state in the volume flags:
> + * mark the volume clean if no volume errors have occurred, and make
> + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
> + * next mount. Also, re-commit all affected inodes.
> */
> if (!sb_rdonly(sb)) {
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb,
> + "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> + } else if (NVolErrors(vol)) {
> + /*
> + * The dirty bit is on disk now; only warn when the
> + * sync actually succeeded, or this message would
> + * contradict the one above.
> + */
> + ntfs_warning(sb,
> + "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> + }
> + /* Commits the updated volume flags if they were written. */
> + ntfs_commit_inode(vol->vol_ino);
> if (!NVolErrors(vol)) {
> - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY))
> - ntfs_warning(sb,
> - "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> - ntfs_commit_inode(vol->vol_ino);
> ntfs_commit_inode(vol->root_ino);
> if (vol->mftmirr_ino)
> ntfs_commit_inode(vol->mftmirr_ino);
> ntfs_commit_inode(vol->mft_ino);
> - } else {
> - ntfs_warning(sb,
> - "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> }
> }
>
> @@ -1886,8 +1935,8 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
> return 0;
>
> /* If there are some dirty buffers in the bdev inode */
> - if (ntfs_clear_volume_dirty_if_no_errors(vol)) {
> - ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> err = -EIO;
> }
> sync_inodes_sb(sb);
> diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
> index bc85a9592245..c7cd27b6dc1a 100644
> --- a/fs/ntfs/volume.h
> +++ b/fs/ntfs/volume.h
> @@ -181,6 +181,8 @@ struct ntfs_volume {
> * Windows-reserved names (CON, AUX, NUL, COM1,
> * LPT1, etc.) or invalid characters.
> *
> + * NV_Hibernated Windows is hibernated on the volume; the sync
> + * paths must not write the volume flags.
> * NV_Discard Issue discard/TRIM commands for freed clusters.
> * NV_DisableSparse Disable creation of sparse regions.
> * NV_NativeSymlinkRel Translate absolute Windows reparse targets (native_symlink=rel).
> @@ -199,6 +201,7 @@ enum {
> NV_ShowHiddenFiles,
> NV_HideDotFiles,
> NV_CheckWindowsNames,
> + NV_Hibernated,
> NV_Discard,
> NV_DisableSparse,
> NV_NativeSymlinkRel,
> @@ -237,6 +240,7 @@ DEFINE_NVOL_BIT_OPS(SysImmutable)
> DEFINE_NVOL_BIT_OPS(ShowHiddenFiles)
> DEFINE_NVOL_BIT_OPS(HideDotFiles)
> DEFINE_NVOL_BIT_OPS(CheckWindowsNames)
> +DEFINE_NVOL_BIT_OPS(Hibernated)
> DEFINE_NVOL_BIT_OPS(Discard)
> DEFINE_NVOL_BIT_OPS(DisableSparse)
> DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state
2026-09-15 8:32 ` [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state Hongling Zeng
2026-09-16 20:23 ` liubaolin
@ 2026-09-16 21:17 ` liubaolin
1 sibling, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 21:17 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> The runtime metadata-corruption paths in fs/ntfs only record the
> in-memory NVolErrors() flag; whether VOLUME_IS_DIRTY ever reaches disk
> depends on ntfs_set_volume_flags() being called by some other path,
> which for most error sites never happens. A volume can therefore
> unmount with a clean on-disk flag despite recorded corruption, and
> chkdsk will not run on the next mount.
>
> Persisting the dirty bit from the error paths themselves does not work:
> they run under a wide variety of ntfs locks, and the dirty-bit write
> takes the $Volume mrec_lock and maps the $Volume mft record, which on
> an $MFT page-cache miss takes the $MFT runlist lock for writing. That
> is enough to self-deadlock or form ABBA cycles from several of them:
> the $MFT extend undo paths hold the $MFT runlist lock and then take
> vol->lcnbmp_lock inside ntfs_cluster_free(); the cluster allocation and
> free rollback paths hold vol->lcnbmp_lock; and the whole mft record
> allocation tree is reachable from ntfs_write_volume_label()'s
> attribute-list maintenance while it holds the $Volume mrec_lock itself.
>
> Instead, make the persistence a property of the sync paths, which run
> without ntfs locks held. The new ntfs_sync_volume_dirty_state() sets
> VOLUME_IS_DIRTY when NVolErrors() is recorded and clears it otherwise,
> evaluating the error flag under the $Volume mrec_lock. It is called
> from ntfs_sync_fs(), from the remount-to-read-only path of
> ntfs_reconfigure(), and from ntfs_put_super(), which previously
> evaluated NVolErrors() outside the lock before clearing the dirty bit
> unconditionally, and which now also persists the dirty bit for volumes
> with recorded errors so they unmount with chkdsk scheduled. The
> ntfs_clear_volume_flags() wrapper, whose last callers this patch
> replaces, has no users left and is removed.
>
> The guarantee this provides is eventual, not instantaneous: the error
> paths record NVolErrors() with a lock-free set_bit(), so a persistence
> point that evaluates the flag just before an error is recorded can
> still leave the on-disk bit clean until the next one. This is sound
> because NVolErrors() is sticky for the lifetime of the mount and every
> persistence point re-derives the on-disk bit from it; the last one,
> ntfs_put_super(), runs after evict_inodes() on a quiesced filesystem,
> so a volume that is read-write at unmount time cannot unmount clean.
> A volume that is already read-only when the error is recorded
> (errors=remount-ro flips the superblock on the first error, as does an
> earlier remount-ro) has no persistence point left and keeps whatever
> on-disk bit it had; that behaviour is unchanged. The residual window
> is a crash between the error and the next persistence point.
>
> The persistence paths never write a hibernated volume: resuming Windows
> from a modified image corrupts it. Record the mount-time hibernation
> verdict in the new NV_Hibernated volume flag and make
> ntfs_sync_volume_dirty_state() a no-op while it is set, so the dirty
> bit is left exactly as it is on disk and only the in-memory error
> state is kept. Without this, an rw mount of a hibernated volume with
> the default errors=continue would gain a filesystem-internal write on
> the first sync, remount or unmount. Other writes to such a mount,
> like the mount-time logfile emptying, are pre-existing and unchanged.
>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/ntfs.h | 1 -
> fs/ntfs/super.c | 129 ++++++++++++++++++++++++++++++++---------------
> fs/ntfs/volume.h | 4 ++
> 3 files changed, 93 insertions(+), 41 deletions(-)
>
> diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h
> index 45f77848a9cf..a5cd5493c501 100644
> --- a/fs/ntfs/ntfs.h
> +++ b/fs/ntfs/ntfs.h
> @@ -219,7 +219,6 @@ struct option_t {
> };
> extern const struct option_t on_errors_arr[];
> int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags);
> -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags);
> int ntfs_write_volume_label(struct ntfs_volume *vol, char *label);
>
> /* From fs/ntfs/mst.c */
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index 6ba19986a598..733565953302 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -262,6 +262,8 @@ static int ntfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
> return 0;
> }
>
> +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol);
> +
> static int ntfs_reconfigure(struct fs_context *fc)
> {
> struct super_block *sb = fc->root->d_sb;
> @@ -312,10 +314,24 @@ static int ntfs_reconfigure(struct fs_context *fc)
> }
> } else if (!sb_rdonly(sb) && (fc->sb_flags & SB_RDONLY)) {
> /* Remounting read-only. */
> - if (!NVolErrors(vol)) {
> - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY))
> - ntfs_warning(sb,
> - "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> + /*
> + * With errors recorded the dirty bit is set rather than
> + * cleared, and it is committed right away: the VFS does
> + * not sync the filesystem during a remount, and once the
> + * remount succeeds no further persistence point exists -
> + * ntfs_sync_fs() is only ever invoked for read-write
> + * superblocks (all its VFS callers skip read-only ones)
> + * and ntfs_put_super() skips them, so the only remaining
> + * write would be the evict-time commit at unmount, which
> + * a crash never reaches. An error recorded only after
> + * the remount is still never persisted.
> + */
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb,
> + "Failed to update dirty bit in volume information flags. Run chkdsk.");
> + } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
> + ntfs_commit_inode(vol->vol_ino);
> + blkdev_issue_flush(sb->s_bdev);
> }
> }
>
> @@ -357,9 +373,10 @@ void ntfs_handle_error(struct super_block *sb)
> * @vol: ntfs volume on which to modify the flags
> * @set_bits: bits to set in the volume information flags
> * @clear_bits: bits to clear in the volume information flags
> + * @dirty_if_errors: force VOLUME_IS_DIRTY on when NVolErrors() is set
> *
> * Internal function. You probably want to use ntfs_{set,clear}_volume_flags()
> - * instead (see below).
> + * or ntfs_sync_volume_dirty_state() instead (see below).
> *
> * Combine @set_bits and @clear_bits with the current in-memory flag state and
> * write the result back. The set/clear helpers pass only the bits to modify,
> @@ -368,11 +385,18 @@ void ntfs_handle_error(struct super_block *sb)
> * All bit manipulation is done on CPU-endian values, and the result is
> * converted back to little-endian before storing it.
> *
> + * When @dirty_if_errors is true and errors have been recorded on @vol,
> + * VOLUME_IS_DIRTY is forced on after the requested changes. NVolErrors() is
> + * evaluated under the same mrec_lock, which orders this against other
> + * locked flag updates; the runtime error paths themselves record the flag
> + * lock-free, so see ntfs_sync_volume_dirty_state() for the guarantee this
> + * provides against them.
> + *
> * Return 0 on success and -errno on error.
> */
> static int ntfs_write_volume_flags(struct ntfs_volume *vol,
> const __le16 set_bits, const __le16 clear_bits,
> - const bool skip_if_errors)
> + const bool dirty_if_errors)
> {
> struct ntfs_inode *ni = NTFS_I(vol->vol_ino);
> struct volume_information *vi;
> @@ -382,12 +406,11 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol,
>
> mutex_lock(&ni->mrec_lock);
>
> - if (skip_if_errors && NVolErrors(vol))
> - goto done;
> -
> flags = le16_to_cpu(vol->vol_flags);
> flags |= le16_to_cpu(set_bits) & le16_to_cpu(VOLUME_FLAGS_MASK);
> flags &= ~(le16_to_cpu(clear_bits) & le16_to_cpu(VOLUME_FLAGS_MASK));
> + if (dirty_if_errors && NVolErrors(vol))
> + flags |= le16_to_cpu(VOLUME_IS_DIRTY);
> ntfs_debug("Entering, old flags = 0x%x, new flags = 0x%x.",
> le16_to_cpu(vol->vol_flags), flags);
>
> @@ -439,31 +462,43 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
> }
>
> /*
> - * ntfs_clear_volume_flags - clear bits in the volume information flags
> - * @vol: ntfs volume on which to modify the flags
> - * @flags: flags to clear on the volume
> + * ntfs_sync_volume_dirty_state - persist the dirty bit per the error state
> + * @vol: ntfs volume whose dirty bit to persist
> *
> - * Clear the bits in @flags in the volume information flags on the volume @vol.
> - * The bits are combined with the current flag state under the lock in
> - * ntfs_write_volume_flags(), so concurrent updates are not lost.
> + * Set VOLUME_IS_DIRTY if errors have been recorded on @vol and clear it
> + * otherwise, under the $Volume mrec_lock.
> *
> - * Return 0 on success and -errno on error.
> - */
> -int ntfs_clear_volume_flags(struct ntfs_volume *vol, __le16 flags)
> -{
> - return ntfs_write_volume_flags(vol, 0, flags, false);
> -}
> -
> -/*
> - * ntfs_clear_volume_dirty_if_no_errors - clear dirty bit if no errors exist
> - * @vol: ntfs volume whose dirty bit should be cleared
> + * The guarantee this provides is eventual, not instantaneous: the runtime
> + * error paths record NVolErrors() with a lock-free set_bit(), so a
> + * persistence point that evaluates the flag just before an error is
> + * recorded can still leave the on-disk bit clean. This is sound because
> + * NVolErrors() is sticky (nothing clears it for the lifetime of the mount)
> + * and every persistence point re-derives the on-disk bit from it; the
> + * last one, ntfs_put_super(), runs after evict_inodes() on a quiesced
> + * filesystem, so a volume that is read-write at unmount time cannot
> + * unmount clean. A volume that is already read-only when the error is
> + * recorded (errors=remount-ro flips the superblock on the first error,
> + * as does an earlier remount-ro) has no persistence point left and
> + * keeps whatever on-disk bit it had; that behaviour is unchanged. The
> + * residual window is a crash between the error and the next
> + * persistence point.
> + *
> + * This is the single point that persists the in-memory error state to disk.
> + * The runtime error paths only record NVolErrors() because they run under a
> + * variety of ntfs locks the dirty-bit write cannot be taken under (runlist
> + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first
> + * ntfs_sync_fs(), a remount, or the unmount then persists the flag here.
> + *
> + * A hibernated volume is not written from these persistence paths:
> + * resuming Windows from a modified image corrupts it, so the dirty bit
> + * is left as it is on disk and only the in-memory error state is kept.
> *
> - * Check NVolErrors() and clear VOLUME_IS_DIRTY under the same mrec_lock so
> - * ntfs_sync_fs() cannot clear the dirty bit after a concurrent error has been
> - * recorded.
> + * Return 0 on success and -errno on error.
> */
> -static int ntfs_clear_volume_dirty_if_no_errors(struct ntfs_volume *vol)
> +static int ntfs_sync_volume_dirty_state(struct ntfs_volume *vol)
> {
> + if (NVolHibernated(vol))
> + return 0;
> return ntfs_write_volume_flags(vol, 0, VOLUME_IS_DIRTY, true);
> }
>
> @@ -1615,6 +1650,11 @@ static bool load_system_files(struct ntfs_volume *vol)
> ntfs_error(sb, "%s. Mounting read-only%s", es1, es2);
> }
> NVolSetErrors(vol);
> + /*
> + * Remember it for the lifetime of the mount: see
> + * ntfs_sync_volume_dirty_state().
> + */
> + NVolSetHibernated(vol);
> }
>
> /* If (still) a read-write mount, empty the logfile. */
> @@ -1772,22 +1812,31 @@ static void ntfs_put_super(struct super_block *sb)
> ntfs_commit_inode(vol->mft_ino);
>
> /*
> - * If a read-write mount and no volume errors have occurred, mark the
> - * volume clean. Also, re-commit all affected inodes.
> + * If a read-write mount, persist the error state in the volume flags:
> + * mark the volume clean if no volume errors have occurred, and make
> + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
> + * next mount. Also, re-commit all affected inodes.
> */
> if (!sb_rdonly(sb)) {
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb,
> + "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> + } else if (NVolErrors(vol)) {
> + /*
> + * The dirty bit is on disk now; only warn when the
> + * sync actually succeeded, or this message would
> + * contradict the one above.
> + */
> + ntfs_warning(sb,
> + "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> + }
> + /* Commits the updated volume flags if they were written. */
> + ntfs_commit_inode(vol->vol_ino);
> if (!NVolErrors(vol)) {
> - if (ntfs_clear_volume_flags(vol, VOLUME_IS_DIRTY))
> - ntfs_warning(sb,
> - "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> - ntfs_commit_inode(vol->vol_ino);
> ntfs_commit_inode(vol->root_ino);
> if (vol->mftmirr_ino)
> ntfs_commit_inode(vol->mftmirr_ino);
> ntfs_commit_inode(vol->mft_ino);
> - } else {
> - ntfs_warning(sb,
> - "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> }
> }
>
> @@ -1886,8 +1935,8 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
> return 0;
>
> /* If there are some dirty buffers in the bdev inode */
> - if (ntfs_clear_volume_dirty_if_no_errors(vol)) {
> - ntfs_warning(sb, "Failed to clear dirty bit in volume information flags. Run chkdsk.");
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> err = -EIO;
> }
> sync_inodes_sb(sb);
> diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
> index bc85a9592245..c7cd27b6dc1a 100644
> --- a/fs/ntfs/volume.h
> +++ b/fs/ntfs/volume.h
> @@ -181,6 +181,8 @@ struct ntfs_volume {
> * Windows-reserved names (CON, AUX, NUL, COM1,
> * LPT1, etc.) or invalid characters.
> *
> + * NV_Hibernated Windows is hibernated on the volume; the sync
> + * paths must not write the volume flags.
> * NV_Discard Issue discard/TRIM commands for freed clusters.
> * NV_DisableSparse Disable creation of sparse regions.
> * NV_NativeSymlinkRel Translate absolute Windows reparse targets (native_symlink=rel).
> @@ -199,6 +201,7 @@ enum {
> NV_ShowHiddenFiles,
> NV_HideDotFiles,
> NV_CheckWindowsNames,
> + NV_Hibernated,
> NV_Discard,
> NV_DisableSparse,
> NV_NativeSymlinkRel,
> @@ -237,6 +240,7 @@ DEFINE_NVOL_BIT_OPS(SysImmutable)
> DEFINE_NVOL_BIT_OPS(ShowHiddenFiles)
> DEFINE_NVOL_BIT_OPS(HideDotFiles)
> DEFINE_NVOL_BIT_OPS(CheckWindowsNames)
> +DEFINE_NVOL_BIT_OPS(Hibernated)
> DEFINE_NVOL_BIT_OPS(Discard)
> DEFINE_NVOL_BIT_OPS(DisableSparse)
> DEFINE_NVOL_BIT_OPS(NativeSymlinkRel)
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 4/7] ntfs: persist the dirty state after the final put_super() commits
2026-09-15 8:32 ` [PATCH v13 4/7] ntfs: persist the dirty state after the final put_super() commits Hongling Zeng
@ 2026-09-16 21:18 ` liubaolin
0 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 21:18 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Baolin Liu, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> The just-in-case mftmirr/mft commits and the final write_inode_now()
> in ntfs_put_super() can record NVolErrors() after the dirty state has
> been persisted, so errors from those points would leave the volume
> unmounted with a clean on-disk dirty bit - contradicting the "cannot
> unmount clean" guarantee ntfs_sync_volume_dirty_state() is meant to
> provide.
>
> Move the persistence to the end of ntfs_put_super(): keep the gated
> re-commits and the tail commits where they are, run
> ntfs_sync_volume_dirty_state() and the $Volume commit after the last
> write_inode_now(), and release vol->vol_ino only after the sync.
>
> The release order of the special inodes matters for the $Volume
> commit: writing the $Volume record mirrors it through
> ntfs_sync_mft_mirror() (record number 3 is below vol->mftmirr_size),
> which fails with -EIO and leaves the mirror stale once
> vol->mftmirr_ino is gone, so the mirror inode is released only after
> that commit. vol->vol_ino is then put before vol->mft_ino is dropped:
> if the commit failed before it could clear the dirty flag,
> ntfs_evict_big_inode() commits the inode again on its way out, and
> __ntfs_write_inode() resolves the runlist through vol->mft_ino.
>
> Reported-by: Baolin Liu <liubaolin@kylinos.cn>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/super.c | 75 ++++++++++++++++++++++++++++++++++---------------
> 1 file changed, 52 insertions(+), 23 deletions(-)
>
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index 733565953302..3574c224fe28 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -1812,26 +1812,13 @@ static void ntfs_put_super(struct super_block *sb)
> ntfs_commit_inode(vol->mft_ino);
>
> /*
> - * If a read-write mount, persist the error state in the volume flags:
> - * mark the volume clean if no volume errors have occurred, and make
> - * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
> - * next mount. Also, re-commit all affected inodes.
> + * If a read-write mount, re-commit all affected inodes once more.
> + * The dirty state itself is persisted at the end of ntfs_put_super(),
> + * after the last commits and the final write_inode_now(): those can
> + * still record errors via __ntfs_write_inode(), and the sync must
> + * evaluate NVolErrors() with the last setter already run.
> */
> if (!sb_rdonly(sb)) {
> - if (ntfs_sync_volume_dirty_state(vol)) {
> - ntfs_warning(sb,
> - "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> - } else if (NVolErrors(vol)) {
> - /*
> - * The dirty bit is on disk now; only warn when the
> - * sync actually succeeded, or this message would
> - * contradict the one above.
> - */
> - ntfs_warning(sb,
> - "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> - }
> - /* Commits the updated volume flags if they were written. */
> - ntfs_commit_inode(vol->vol_ino);
> if (!NVolErrors(vol)) {
> ntfs_commit_inode(vol->root_ino);
> if (vol->mftmirr_ino)
> @@ -1840,9 +1827,6 @@ static void ntfs_put_super(struct super_block *sb)
> }
> }
>
> - iput(vol->vol_ino);
> - vol->vol_ino = NULL;
> -
> /* NTFS 3.0+ specific clean up. */
> if (vol->major_ver >= 3) {
> if (vol->extend_ino) {
> @@ -1872,8 +1856,6 @@ static void ntfs_put_super(struct super_block *sb)
> /* Re-commit the mft mirror and mft just in case. */
> ntfs_commit_inode(vol->mftmirr_ino);
> ntfs_commit_inode(vol->mft_ino);
> - iput(vol->mftmirr_ino);
> - vol->mftmirr_ino = NULL;
> }
> /*
> * We should have no dirty inodes left, due to
> @@ -1883,6 +1865,53 @@ static void ntfs_put_super(struct super_block *sb)
> ntfs_commit_inode(vol->mft_ino);
> write_inode_now(vol->mft_ino, 1);
>
> + /*
> + * If a read-write mount, persist the error state in the volume flags:
> + * mark the volume clean if no volume errors have occurred, and make
> + * sure VOLUME_IS_DIRTY is on disk if any have, so chkdsk runs on the
> + * next mount.
> + */
> + if (!sb_rdonly(sb)) {
> + if (ntfs_sync_volume_dirty_state(vol)) {
> + ntfs_warning(sb,
> + "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> + } else if (NVolErrors(vol)) {
> + /*
> + * The dirty bit is on disk now; only warn when the
> + * sync actually succeeded, or this message would
> + * contradict the one above.
> + */
> + ntfs_warning(sb,
> + "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> + }
> + /*
> + * Commits the updated volume flags if they were written.
> + * The mft mirror must still be around for this: the
> + * $Volume record (mft record number 3, below
> + * vol->mftmirr_size) is mirrored by write_mft_record()
> + * through ntfs_sync_mft_mirror(), which fails with -EIO
> + * and leaves the mirror stale once vol->mftmirr_ino is
> + * gone, so the mirror inode is only released after this
> + * commit.
> + */
> + ntfs_commit_inode(vol->vol_ino);
> + }
> +
> + /*
> + * Release $Volume while the mft inode is still available: if the
> + * commit above failed before it could clear the dirty flag,
> + * ntfs_evict_big_inode() commits the inode again on its way out,
> + * and __ntfs_write_inode() needs vol->mft_ino to look up the
> + * runlist of the record to write.
> + */
> + iput(vol->vol_ino);
> + vol->vol_ino = NULL;
> +
> + if (vol->mftmirr_ino) {
> + iput(vol->mftmirr_ino);
> + vol->mftmirr_ino = NULL;
> + }
> +
> iput(vol->mft_ino);
> vol->mft_ino = NULL;
> blkdev_issue_flush(sb->s_bdev);
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 5/7] ntfs: do not clear the volume dirty bit during sync
2026-09-15 8:32 ` [PATCH v13 5/7] ntfs: do not clear the volume dirty bit during sync Hongling Zeng
@ 2026-09-16 21:18 ` liubaolin
0 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 21:18 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Baolin Liu, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> ntfs_sync_fs() clears VOLUME_IS_DIRTY while the volume is still mounted
> read-write, so a sync running concurrently with an in-flight metadata
> modification can clear and persist a bit that was just set: the
> modification then lands on a volume that is clean on disk, and a crash
> does not run chkdsk.
>
> Stop clearing the bit in ntfs_sync_fs() and leave the clearing to the
> remount-to-read-only path, which the VFS reaches only after
> sb_prepare_remount_readonly() has drained in-flight writers, and to
> ntfs_put_super(), which runs after evict_inodes() on a quiesced
> filesystem. A mounted read-write volume now keeps the dirty bit until
> it is dismounted cleanly, which matches the NTFS semantics; the cost is
> a needless chkdsk if the machine crashes between a sync and the unmount.
>
> A recorded error state is still persisted right away when the
> filesystem is synced: with NVolErrors() set,
> ntfs_sync_volume_dirty_state() can only set the bit, never clear it,
> so it cannot lose a modification the way the old unconditional call
> did. This keeps the error report from being lost to a crash on a
> volume that has seen no modification; an error recorded after the
> last sync is still only persisted at the next quiescent transition.
>
> sync_blockdev() and blkdev_issue_flush() are both called and the first
> error is returned, so a writeback failure neither hides a flush failure
> nor skips it.
>
> A RWF_NOWAIT write still blocks in the marking when the volume looks
> clean, as it already did on the base; a non-blocking marking is
> follow-up work.
>
> Reported-by: Baolin Liu <liubaolin@kylinos.cn>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/file.c | 7 ++++---
> fs/ntfs/super.c | 31 ++++++++++++++++++++++++-------
> 2 files changed, 28 insertions(+), 10 deletions(-)
>
> diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
> index cfc7b36b7dff..99a2c7a5cf81 100644
> --- a/fs/ntfs/file.c
> +++ b/fs/ntfs/file.c
> @@ -621,9 +621,10 @@ static ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
>
> /*
> * The volume must be marked dirty before the modification is made,
> - * without an unlocked check of the in-memory flag: ntfs_sync_fs()
> - * can clear the bit concurrently and the modification would then
> - * land on a volume that is clean on disk.
> + * without an unlocked check of the in-memory flag: the dirty bit
> + * is only cleared at the quiescent transitions, under the same
> + * $Volume mrec_lock this call takes, so an unlocked skip could
> + * lose the set to one of them.
> */
> ntfs_set_volume_flags(vol, VOLUME_IS_DIRTY);
>
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index 3574c224fe28..463050ae1b0e 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -486,8 +486,9 @@ int ntfs_set_volume_flags(struct ntfs_volume *vol, __le16 flags)
> * This is the single point that persists the in-memory error state to disk.
> * The runtime error paths only record NVolErrors() because they run under a
> * variety of ntfs locks the dirty-bit write cannot be taken under (runlist
> - * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); the first
> - * ntfs_sync_fs(), a remount, or the unmount then persists the flag here.
> + * locks, vol->lcnbmp_lock, vol->mftbmp_lock, mrec_locks); a sync of a
> + * volume with recorded errors, a remount to read-only, or the unmount,
> + * then persists the flag here.
> *
> * A hibernated volume is not written from these persistence paths:
> * resuming Windows from a modified image corrupts it, so the dirty bit
> @@ -1955,7 +1956,7 @@ static void ntfs_shutdown(struct super_block *sb)
> static int ntfs_sync_fs(struct super_block *sb, int wait)
> {
> struct ntfs_volume *vol = NTFS_SB(sb);
> - int err = 0;
> + int ret, err = 0;
>
> if (NVolShutdown(vol))
> return -EIO;
> @@ -1963,14 +1964,30 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
> if (!wait)
> return 0;
>
> - /* If there are some dirty buffers in the bdev inode */
> - if (ntfs_sync_volume_dirty_state(vol)) {
> + /*
> + * The volume dirty bit is deliberately not cleared here: a sync
> + * running concurrently with an in-flight modification could clear
> + * and persist a bit that was just set, leaving the modification
> + * on a volume that is clean on disk. The bit is only cleared at
> + * the quiescent state transitions, remounting read-only and clean
> + * unmount. A recorded error state, however, is persisted right
> + * away so that it is not lost to a crash on a volume that has
> + * seen no modification; with NVolErrors() set this can only set
> + * the bit, never clear it.
> + */
> + if (NVolErrors(vol) &&
> + ntfs_sync_volume_dirty_state(vol)) {
> ntfs_warning(sb, "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> err = -EIO;
> }
> sync_inodes_sb(sb);
> - sync_blockdev(sb->s_bdev);
> - blkdev_issue_flush(sb->s_bdev);
> + ret = sync_blockdev(sb->s_bdev);
> + if (ret && !err)
> + err = ret;
> +
> + ret = blkdev_issue_flush(sb->s_bdev);
> + if (ret && !err)
> + err = ret;
> return err;
> }
>
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 6/7] ntfs: check the dirty-state commit on remount and unmount
2026-09-15 8:32 ` [PATCH v13 6/7] ntfs: check the dirty-state commit on remount and unmount Hongling Zeng
@ 2026-09-16 21:18 ` liubaolin
0 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 21:18 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, Baolin Liu, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> The remount-to-read-only path commits the updated volume flags with
> ntfs_commit_inode(), a void wrapper around __ntfs_write_inode(), and
> ignores the blkdev_issue_flush() return value, so a failed commit or
> flush is reported as success. Once the remount has succeeded no
> persistence point is ever reached again: ntfs_put_super() skips
> read-only superblocks and the VFS never syncs one, so fail the
> remount unless the commit and the flush succeed. The superblock
> then stays read-write and ntfs_put_super() retries the persistence
> at unmount. The errors=remount-ro downgrade does not go through
> ntfs_reconfigure() and is unchanged.
>
> A zero-return commit is not trusted blindly: write_mft_record()
> redirties the record on allocation failure and reports success, so
> the $Volume inode is required to be clean afterwards.
>
> ntfs_put_super() discards the same commit error. Call
> __ntfs_write_inode() there with the same dirty re-check and warn on
> failure, as put_super() cannot return an error. The commit is
> skipped when the dirty-state sync itself failed, as that could write
> back an inconsistent flag state; a record left dirty by an earlier
> update is still committed at evict time.
>
> NVolErrors() is deliberately not used to detect the failure: it is
> sticky for the lifetime of the mount, so it cannot distinguish a
> fresh commit failure from errors recorded before the remount.
>
> Hibernated volumes: ntfs_sync_volume_dirty_state() is a no-op for
> them and never dirties the $Volume inode on such a mount, since the
> on-disk flags are already dirty and ntfs_set_volume_flags() has
> nothing to change. The commit only runs if something dirtied the
> inode independently, as before this patch; mounting hibernated
> volumes read-only removes even that.
>
> Reported-by: Baolin Liu <liubaolin@kylinos.cn>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/super.c | 76 +++++++++++++++++++++++++++++++++++--------------
> 1 file changed, 54 insertions(+), 22 deletions(-)
>
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index 463050ae1b0e..0228d7429596 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -268,6 +268,7 @@ static int ntfs_reconfigure(struct fs_context *fc)
> {
> struct super_block *sb = fc->root->d_sb;
> struct ntfs_volume *vol = NTFS_SB(sb);
> + int err;
>
> ntfs_debug("Entering with remount");
>
> @@ -324,14 +325,39 @@ static int ntfs_reconfigure(struct fs_context *fc)
> * and ntfs_put_super() skips them, so the only remaining
> * write would be the evict-time commit at unmount, which
> * a crash never reaches. An error recorded only after
> - * the remount is still never persisted.
> + * the remount is still never persisted; a failed commit
> + * or flush fails the remount, leaving the superblock
> + * read-write so ntfs_put_super() retries at unmount.
> */
> - if (ntfs_sync_volume_dirty_state(vol)) {
> + err = ntfs_sync_volume_dirty_state(vol);
> + if (err) {
> ntfs_warning(sb,
> "Failed to update dirty bit in volume information flags. Run chkdsk.");
> - } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
> - ntfs_commit_inode(vol->vol_ino);
> - blkdev_issue_flush(sb->s_bdev);
> + return err;
> + }
> + if (NInoDirty(NTFS_I(vol->vol_ino))) {
> + /* ntfs_commit_inode() would discard the error. */
> + err = __ntfs_write_inode(vol->vol_ino, 1);
> + if (err) {
> + ntfs_warning(sb,
> + "Failed to commit volume information flags. Run chkdsk.");
> + return err;
> + }
> + /*
> + * write_mft_record() redirties the record on
> + * -ENOMEM and still reports success.
> + */
> + if (NInoDirty(NTFS_I(vol->vol_ino))) {
> + ntfs_warning(sb,
> + "Volume information flags remain dirty after commit. Run chkdsk.");
> + return -EIO;
> + }
> + err = blkdev_issue_flush(sb->s_bdev);
> + if (err) {
> + ntfs_warning(sb,
> + "Failed to flush volume information flags. Run chkdsk.");
> + return err;
> + }
> }
> }
>
> @@ -1876,26 +1902,32 @@ static void ntfs_put_super(struct super_block *sb)
> if (ntfs_sync_volume_dirty_state(vol)) {
> ntfs_warning(sb,
> "Failed to sync dirty bit in volume information flags. Run chkdsk.");
> - } else if (NVolErrors(vol)) {
> + } else {
> /*
> - * The dirty bit is on disk now; only warn when the
> - * sync actually succeeded, or this message would
> - * contradict the one above.
> + * __ntfs_write_inode(), not the void
> + * ntfs_commit_inode() wrapper: the error can only
> + * be warned about here. The mirror inode is only
> + * released below: writing the $Volume record (mft
> + * record number 3, below vol->mftmirr_size) mirrors
> + * it through ntfs_sync_mft_mirror(), which fails
> + * with -EIO once vol->mftmirr_ino is gone.
> */
> - ntfs_warning(sb,
> - "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> + if (__ntfs_write_inode(vol->vol_ino, 1)) {
> + ntfs_warning(sb,
> + "Failed to commit volume information flags. Run chkdsk.");
> + } else if (NInoDirty(NTFS_I(vol->vol_ino))) {
> + ntfs_warning(sb,
> + "Volume information flags remain dirty after commit. Run chkdsk.");
> + } else if (NVolErrors(vol)) {
> + /*
> + * Only warn once the commit has succeeded,
> + * or this could contradict a failure
> + * reported above.
> + */
> + ntfs_warning(sb,
> + "Volume has errors. Leaving volume marked dirty. Run chkdsk.");
> + }
> }
> - /*
> - * Commits the updated volume flags if they were written.
> - * The mft mirror must still be around for this: the
> - * $Volume record (mft record number 3, below
> - * vol->mftmirr_size) is mirrored by write_mft_record()
> - * through ntfs_sync_mft_mirror(), which fails with -EIO
> - * and leaves the mirror stale once vol->mftmirr_ino is
> - * gone, so the mirror inode is only released after this
> - * commit.
> - */
> - ntfs_commit_inode(vol->vol_ino);
> }
>
> /*
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 7/7] ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE
2026-09-15 8:32 ` [PATCH v13 7/7] ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE Hongling Zeng
@ 2026-09-16 21:18 ` liubaolin
0 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 21:18 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee
Cc: ntfs, linux-kernel, zhongling0719, stable
在 2026/9/15 16:32, Hongling Zeng 写道:
> ntfs_reconfigure() currently ignores sync_filesystem() errors, allowing
> a regular read-only remount to succeed even when dirty data was not
> synced. Check the error and fail regular remounts, leaving the
> superblock read-write so ntfs_put_super() can retry at unmount.
>
> SB_FORCE does not wait for writers already in progress, so it must not
> clear the on-disk dirty bit. Warn and continue on sync errors for
> SB_FORCE. A forced remount updates the dirty state only when errors
> have been recorded, and never clears the dirty bit. Skip the commit if
> that state update fails, since the in-memory flags may be inconsistent.
>
> Reported-by: Hyunchul Lee <hyc.lee@gmail.com>
> Cc: stable@vger.kernel.org
> Signed-off-by: Hongling Zeng <zenghongling@kylinos.cn>
> ---
> fs/ntfs/super.c | 27 +++++++++++++++++++++------
> 1 file changed, 21 insertions(+), 6 deletions(-)
>
> diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
> index 0228d7429596..82b5bf28b6ab 100644
> --- a/fs/ntfs/super.c
> +++ b/fs/ntfs/super.c
> @@ -272,7 +272,13 @@ static int ntfs_reconfigure(struct fs_context *fc)
>
> ntfs_debug("Entering with remount");
>
> - sync_filesystem(sb);
> + err = sync_filesystem(sb);
> + if (err) {
> + ntfs_warning(sb, "Failed to sync the filesystem.");
> + /* A forced remount must still turn the superblock read-only. */
> + if (!(fc->sb_flags & SB_FORCE))
> + return err;
> + }
>
> /*
> * For the read-write compiled driver, if we are remounting read-write,
> @@ -329,11 +335,20 @@ static int ntfs_reconfigure(struct fs_context *fc)
> * or flush fails the remount, leaving the superblock
> * read-write so ntfs_put_super() retries at unmount.
> */
> - err = ntfs_sync_volume_dirty_state(vol);
> - if (err) {
> - ntfs_warning(sb,
> - "Failed to update dirty bit in volume information flags. Run chkdsk.");
> - return err;
> + /*
> + * A forced remount does not drain writers in progress,
> + * so one may still be modifying metadata when the flags
> + * are committed: never clear the dirty bit then; if
> + * errors have been recorded, the update preserves or
> + * sets it; otherwise, skip the update entirely.
> + */
> + if (!(fc->sb_flags & SB_FORCE) || NVolErrors(vol)) {
> + err = ntfs_sync_volume_dirty_state(vol);
> + if (err) {
> + ntfs_warning(sb,
> + "Failed to update dirty bit in volume information flags. Run chkdsk.");
> + return err;
> + }
> }
> if (NInoDirty(NTFS_I(vol->vol_ino))) {
> /* ntfs_commit_inode() would discard the error. */
Looks good to me.
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (7 preceding siblings ...)
2026-09-16 0:42 ` [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hyunchul Lee
@ 2026-09-16 21:36 ` liubaolin
2026-09-16 23:36 ` Namjae Jeon
9 siblings, 0 replies; 19+ messages in thread
From: liubaolin @ 2026-09-16 21:36 UTC (permalink / raw)
To: Hongling Zeng, linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, zhongling0719
After several rounds of review and Hongling's revisions, I think the
series is now in good shape.
For the whole series:
Reviewed-by: Baolin Liu <liubaolin@kylinos.cn>
在 2026/9/15 16:32, Hongling Zeng 写道:
> Hi all,
>
> The fs/ntfs runtime metadata-corruption paths only record the in-memory
> NVolErrors() flag, and the dirty-bit persistence used to race with
> ntfs_sync_fs(): a volume could end up with a clean on-disk dirty flag
> despite modification or recorded corruption, so chkdsk would not run on
> the next mount. This series fixes that.
>
> 1/6 makes the volume flag read-modify-write atomic under the
> $Volume mrec_lock;
> 2/6 marks the volume dirty unconditionally on metadata changes,
> dropping the racy caller-side checks in file.c and namei.c;
> 3/6 derives the on-disk dirty bit from the recorded error state at
> the persistence points (sync_fs, remount-ro, put_super) and never
> writes a hibernated volume;
> 4/6 persists the dirty state after the final put_super() commits so
> late errors cannot unmount clean;
> 5/6 stops ntfs_sync_fs() from clearing VOLUME_IS_DIRTY: the clearing
> moves to the quiescent transitions, a recorded error state is
> still persisted at sync time, and sync now reports writeback and
> flush errors instead of discarding them;
> 6/6 checks the dirty-state commit on remount and unmount.
> 7/7 ntfs: fail remount on sync errors and keep the dirty bit on
> SB_FORCE
>
> Changes in this revision, from the review:
>
> - 5/6: with the clearing gone from the sync path, a recorded error
> state is persisted without ever clearing the bit, and
> sync_blockdev() and blkdev_issue_flush() are both called with the
> first error returned.
>
> - The IOCB_NOWAIT behavior and the per-operation $Volume mrec_lock
> acquisition are outside the scope of this series. The series keeps
> the unconditional ntfs_set_volume_flags() call to preserve the
> ordering that marks the volume dirty before the metadata
> modification; a RWF_NOWAIT write still blocks in the marking when
> the volume looks clean, as it already did on the base. The
> non-blocking and contended-lock handling (mutex_trylock, GFP_NOWAIT)
> will be addressed in a separate follow-up patch.
>
> Hongling Zeng (4):
> ntfs: fix volume flag update races
> ntfs: set the volume dirty bit unconditionally on metadata changes
> ntfs: sync the volume dirty bit with the recorded error state
> ntfs: persist the dirty state after the final put_super() commits
>
> fs/ntfs/file.c | 20 ++---
> fs/ntfs/namei.c | 24 ++----
> fs/ntfs/ntfs.h | 1 -
> fs/ntfs/super.c | 191 ++++++++++++++++++++++++++++++++++++-----------
> fs/ntfs/volume.h | 4 +
> 5 files changed, 171 insertions(+), 69 deletions(-)
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
` (8 preceding siblings ...)
2026-09-16 21:36 ` liubaolin
@ 2026-09-16 23:36 ` Namjae Jeon
9 siblings, 0 replies; 19+ messages in thread
From: Namjae Jeon @ 2026-09-16 23:36 UTC (permalink / raw)
To: Hongling Zeng; +Cc: hyc.lee, ntfs, linux-kernel, zhongling0719
On Tue, Sep 15, 2026 at 5:33 PM Hongling Zeng <zenghongling@kylinos.cn> wrote:
>
> Hi all,
>
> The fs/ntfs runtime metadata-corruption paths only record the in-memory
> NVolErrors() flag, and the dirty-bit persistence used to race with
> ntfs_sync_fs(): a volume could end up with a clean on-disk dirty flag
> despite modification or recorded corruption, so chkdsk would not run on
> the next mount. This series fixes that.
>
> 1/6 makes the volume flag read-modify-write atomic under the
> $Volume mrec_lock;
> 2/6 marks the volume dirty unconditionally on metadata changes,
> dropping the racy caller-side checks in file.c and namei.c;
> 3/6 derives the on-disk dirty bit from the recorded error state at
> the persistence points (sync_fs, remount-ro, put_super) and never
> writes a hibernated volume;
> 4/6 persists the dirty state after the final put_super() commits so
> late errors cannot unmount clean;
> 5/6 stops ntfs_sync_fs() from clearing VOLUME_IS_DIRTY: the clearing
> moves to the quiescent transitions, a recorded error state is
> still persisted at sync time, and sync now reports writeback and
> flush errors instead of discarding them;
> 6/6 checks the dirty-state commit on remount and unmount.
> 7/7 ntfs: fail remount on sync errors and keep the dirty bit on
> SB_FORCE
>
> Changes in this revision, from the review:
>
> - 5/6: with the clearing gone from the sync path, a recorded error
> state is persisted without ever clearing the bit, and
> sync_blockdev() and blkdev_issue_flush() are both called with the
> first error returned.
>
> - The IOCB_NOWAIT behavior and the per-operation $Volume mrec_lock
> acquisition are outside the scope of this series. The series keeps
> the unconditional ntfs_set_volume_flags() call to preserve the
> ordering that marks the volume dirty before the metadata
> modification; a RWF_NOWAIT write still blocks in the marking when
> the volume looks clean, as it already did on the base. The
> non-blocking and contended-lock handling (mutex_trylock, GFP_NOWAIT)
> will be addressed in a separate follow-up patch.
>
> Hongling Zeng (4):
> ntfs: fix volume flag update races
> ntfs: set the volume dirty bit unconditionally on metadata changes
> ntfs: sync the volume dirty bit with the recorded error state
> ntfs: persist the dirty state after the final put_super() commits
Applied them to #ntfs-next.
Thanks!
^ permalink raw reply [flat|nested] 19+ messages in thread
end of thread, other threads:[~2026-09-16 23:36 UTC | newest]
Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-15 8:32 [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hongling Zeng
2026-09-15 8:32 ` [PATCH v13 1/7] ntfs: fix volume flag update races Hongling Zeng
2026-09-16 20:23 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 2/7] ntfs: set the volume dirty bit unconditionally on metadata changes Hongling Zeng
2026-09-16 20:23 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 3/7] ntfs: sync the volume dirty bit with the recorded error state Hongling Zeng
2026-09-16 20:23 ` liubaolin
2026-09-16 21:17 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 4/7] ntfs: persist the dirty state after the final put_super() commits Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 5/7] ntfs: do not clear the volume dirty bit during sync Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 6/7] ntfs: check the dirty-state commit on remount and unmount Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-15 8:32 ` [PATCH v13 7/7] ntfs: fail remount on sync errors and keep the dirty bit on SB_FORCE Hongling Zeng
2026-09-16 21:18 ` liubaolin
2026-09-16 0:42 ` [PATCH v13 0/7] ntfs: fix volume flag races and persist the recorded error state Hyunchul Lee
2026-09-16 21:36 ` liubaolin
2026-09-16 23:36 ` Namjae Jeon
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®