]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
ntfs: fix mrec_lock ABBA deadlock in rename
authorPeiyang He <peiyang_he@smail.nju.edu.cn>
Tue, 30 Jun 2026 03:08:56 +0000 (11:08 +0800)
committerNamjae Jeon <linkinjeon@kernel.org>
Tue, 30 Jun 2026 14:22:34 +0000 (23:22 +0900)
ntfs_file_fsync(), ntfs_dir_fsync() and __ntfs_write_inode() lock an
inode's mrec_lock before taking the mrec_lock of its parent directory.

ntfs_rename() takes old_ni->mrec_lock and old_dir_ni->mrec_lock
before taking new_ni->mrec_lock for an existing target, or
new_dir_ni->mrec_lock for a cross-directory rename.
This can deadlock when ntfs_file_fsync() or __ntfs_write_inode() holds
the target inode, or when ntfs_dir_fsync() holds a child target
directory, while rename() holds the parent directory and waits for the
target.

Fix this by locking the existing target inode before taking any parent
directory mrec_lock. For cross-directory renames where the target parent
is a descendant of the source parent, lock the target parent before the
source parent so the directory order matches the child-to-parent order used
by ntfs_file_fsync(), ntfs_dir_fsync(), and __ntfs_write_inode().

Reported-by: Peiyang He <peiyang_he@smail.nju.edu.cn>
Closes: https://lore.kernel.org/all/C4D296F0E9F3D66C+9397ffbc-eb55-44bb-9b3f-5da4809e7955@smail.nju.edu.cn/
Fixes: af0db57d4293 ("ntfs: update inode operations")
Cc: stable@vger.kernel.org
Signed-off-by: Peiyang He <peiyang_he@smail.nju.edu.cn>
Assisted-by: Codex:gpt-5.5
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
fs/ntfs/namei.c

index a19626a135bd7fe957c46f85b491c54ca68e5c10..5ff25e9aaa325ae0e61dcdb6c8bf47471c26ef24 100644 (file)
@@ -1266,6 +1266,7 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
        struct ntfs_volume *vol = NTFS_SB(sb);
        struct ntfs_inode *old_ni, *new_ni = NULL;
        struct ntfs_inode *old_dir_ni = NTFS_I(old_dir), *new_dir_ni = NTFS_I(new_dir);
+       bool new_dir_first = false;
 
        if (NVolShutdown(old_dir_ni->vol))
                return -EIO;
@@ -1301,36 +1302,39 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
        old_inode = old_dentry->d_inode;
        new_inode = new_dentry->d_inode;
        old_ni = NTFS_I(old_inode);
+       if (new_inode)
+               new_ni = NTFS_I(new_inode);
+       if (old_dir != new_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);
 
        mutex_lock_nested(&old_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
-       mutex_lock_nested(&old_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT);
+       if (new_ni)
+               mutex_lock_nested(&new_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL_2);
+
+       if (old_dir == new_dir) {
+               mutex_lock_nested(&old_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT);
+       } else if (new_dir_first) {
+               mutex_lock_nested(&new_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT);
+               mutex_lock_nested(&old_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT_2);
+       } else {
+               mutex_lock_nested(&old_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT);
+               mutex_lock_nested(&new_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT_2);
+       }
 
-       if (NInoBeingDeleted(old_ni) || NInoBeingDeleted(old_dir_ni)) {
+       if (NInoBeingDeleted(old_ni) || NInoBeingDeleted(old_dir_ni) ||
+           (new_ni && NInoBeingDeleted(new_ni)) ||
+           (old_dir != new_dir && NInoBeingDeleted(new_dir_ni))) {
                err = -ENOENT;
-               goto unlock_old;
+               goto err_out;
        }
 
        is_dir = S_ISDIR(old_inode->i_mode);
 
        if (new_inode) {
-               new_ni = NTFS_I(new_inode);
-               mutex_lock_nested(&new_ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL_2);
-               if (old_dir != new_dir) {
-                       mutex_lock_nested(&new_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT_2);
-                       if (NInoBeingDeleted(new_dir_ni)) {
-                               err = -ENOENT;
-                               goto err_out;
-                       }
-               }
-
-               if (NInoBeingDeleted(new_ni)) {
-                       err = -ENOENT;
-                       goto err_out;
-               }
-
                if (is_dir) {
                        struct mft_record *ni_mrec;
 
@@ -1348,14 +1352,6 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
                err = ntfs_delete(new_ni, new_dir_ni, uname_new, new_name_len, false);
                if (err)
                        goto err_out;
-       } else {
-               if (old_dir != new_dir) {
-                       mutex_lock_nested(&new_dir_ni->mrec_lock, NTFS_INODE_MUTEX_PARENT_2);
-                       if (NInoBeingDeleted(new_dir_ni)) {
-                               err = -ENOENT;
-                               goto err_out;
-                       }
-               }
        }
 
        err = __ntfs_link(old_ni, new_dir_ni, uname_new, new_name_len);
@@ -1386,13 +1382,17 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
        inode_inc_iversion(new_dir);
 
 err_out:
-       if (old_dir != new_dir)
+       if (old_dir == new_dir) {
+               mutex_unlock(&old_dir_ni->mrec_lock);
+       } else if (new_dir_first) {
+               mutex_unlock(&old_dir_ni->mrec_lock);
                mutex_unlock(&new_dir_ni->mrec_lock);
-       if (new_inode)
+       } else {
+               mutex_unlock(&new_dir_ni->mrec_lock);
+               mutex_unlock(&old_dir_ni->mrec_lock);
+       }
+       if (new_ni)
                mutex_unlock(&new_ni->mrec_lock);
-
-unlock_old:
-       mutex_unlock(&old_dir_ni->mrec_lock);
        mutex_unlock(&old_ni->mrec_lock);
        if (uname_new)
                kmem_cache_free(ntfs_name_cache, uname_new);