[PATCH 5/6] fs/ntfs3: walk $FILE_NAME under ni_lock in ni_write_parents()
From: Kamil Chojnowski via B4 Relay
Date: Thu Oct 29 2026 - 20:10:41 EST
From: Kamil Chojnowski <contact@xxxxxxxxx>
ni_write_parents() (fsync) walks the inode's attributes without ni_lock.
When the record is rearranged at the same time - a name removed by
unlink, $DATA moved out of the record by a write through another file
descriptor - it can read a half-moved attribute. It then either follows
a garbage parent reference:
ntfs3(vda): MFT: r=0, expect seq=0 instead of 1!
ntfs3(vda): ino=1e6, failed to open parent directory r=0 to write
or mi_enum_attr() sees an inconsistent record and marks a healthy inode
bad and the volume dirty, after which every operation on the file fails
with -EINVAL:
ntfs3(vda): ino=100, "res2" mi_enum_attr
Collect the parent references under ni_lock and open and write the
parents after dropping it.
Reproducer: overwrite a small file through a shared mmap, grow it past
the MFT record with pwrite(2) and fsync(2) it, while another thread
calls fdatasync(2) on it in a loop.
Fixes: dcd9d6a47199 ("fs/ntfs3: fsync files by syncing parent inodes")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Kamil Chojnowski <contact@xxxxxxxxx>
---
fs/ntfs3/frecord.c | 42 +++++++++++++++++++++++++++++++++++-------
1 file changed, 35 insertions(+), 7 deletions(-)
diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c
index 3d10b1519c86..a121ca4db0c0 100644
--- a/fs/ntfs3/frecord.c
+++ b/fs/ntfs3/frecord.c
@@ -2997,13 +2997,33 @@ int ni_write_parents(struct ntfs_inode *ni, int sync)
struct ATTR_LIST_ENTRY *le = NULL;
struct ntfs_sb_info *sbi = ni->mi.sbi;
struct super_block *sb = sbi->sb;
+ struct MFT_REF *homes;
+ u32 i, n = 0, cnt = 0;
if (!is_ni_base(ni))
return 0;
+ /*
+ * Collect the parent references under ni_lock: the record may be
+ * rearranged at the same time (a name removed, $DATA moved out of the
+ * record), and mi_enum_attr() marks the inode bad if it walks a
+ * half-moved attribute. Open and write the parents without ni_lock.
+ */
+ ni_lock(ni);
while ((attr = ni_find_attr(ni, attr, &le, ATTR_NAME, NULL, 0, NULL,
- NULL))) {
- struct inode *dir;
+ NULL)))
+ cnt++;
+
+ homes = kmalloc_objs(*homes, cnt, GFP_NOFS);
+ if (cnt && !homes) {
+ ni_unlock(ni);
+ return -ENOMEM;
+ }
+
+ attr = NULL;
+ le = NULL;
+ while (n < cnt && (attr = ni_find_attr(ni, attr, &le, ATTR_NAME, NULL,
+ 0, NULL, NULL))) {
struct ATTR_FILE_NAME *fname;
fname = resident_data_ex(attr, SIZEOF_ATTRIBUTE_FILENAME);
@@ -3012,19 +3032,26 @@ int ni_write_parents(struct ntfs_inode *ni, int sync)
/* Check simple case when parent inode equals current inode. */
if (ino_get(&fname->home) == ni->vfs_inode.i_ino) {
- if (MFT_REC_ROOT != ni->vfs_inode.i_ino) {
- ntfs_set_state(sbi, NTFS_DIRTY_ERROR);
+ if (ni->vfs_inode.i_ino != MFT_REC_ROOT)
err = -EINVAL;
- }
continue;
}
- dir = ntfs_iget5(sb, &fname->home, NULL);
+ homes[n++] = fname->home;
+ }
+ ni_unlock(ni);
+
+ if (err)
+ ntfs_set_state(sbi, NTFS_DIRTY_ERROR);
+
+ for (i = 0; i < n; i++) {
+ struct inode *dir = ntfs_iget5(sb, &homes[i], NULL);
+
if (IS_ERR(dir)) {
ntfs_inode_warn(
&ni->vfs_inode,
"failed to open parent directory r=%llx to write",
- (u64)ino_get(&fname->home));
+ (u64)ino_get(&homes[i]));
continue;
}
@@ -3035,6 +3062,7 @@ int ni_write_parents(struct ntfs_inode *ni, int sync)
}
iput(dir);
}
+ kfree(homes);
return err;
}
--
2.56.0