diff --git a/fs/fat/namei_vfat.c b/fs/fat/namei_vfat.c index 88ccb2ee3537bc..d7127c82a672d7 100644 --- a/fs/fat/namei_vfat.c +++ b/fs/fat/namei_vfat.c @@ -1017,13 +1017,168 @@ static int vfat_rename(struct inode *old_dir, struct dentry *old_dentry, goto out; } +static int vfat_rename_exchange(struct inode *old_dir, struct dentry *old_dentry, + struct inode *new_dir, struct dentry *new_dentry) +{ + struct buffer_head *old_dotdot_bh = NULL, *new_dotdot_bh = NULL; + struct msdos_dir_entry *old_dotdot_de = NULL, *new_dotdot_de = NULL; + struct inode *old_inode, *new_inode; + struct timespec64 ts = current_time(old_dir); + loff_t old_i_pos, new_i_pos; + int err, corrupt = 0; + struct super_block *sb = old_dir->i_sb; + + old_inode = d_inode(old_dentry); + new_inode = d_inode(new_dentry); + + /* Acquire super block lock for the operation to be atomic */ + mutex_lock(&MSDOS_SB(sb)->s_lock); + + /* if directories are not the same, get ".." info to update */ + if (old_dir != new_dir) { + if (S_ISDIR(old_inode->i_mode)) + if (fat_get_dotdot_entry(old_inode, &old_dotdot_bh, &old_dotdot_de)) { + err = -EIO; + goto out; + } + + if (S_ISDIR(new_inode->i_mode)) + if (fat_get_dotdot_entry(new_inode, &new_dotdot_bh, &new_dotdot_de)) { + err = -EIO; + goto out; + } + } + + /* exchange the two dentries */ + old_i_pos = MSDOS_I(old_inode)->i_pos; + new_i_pos = MSDOS_I(new_inode)->i_pos; + + fat_detach(old_inode); + fat_detach(new_inode); + + fat_attach(old_inode, new_i_pos); + fat_attach(new_inode, old_i_pos); + + if (IS_DIRSYNC(old_dir)) { + err = fat_sync_inode(new_inode); + if (err) + goto error_exchange; + } else { + mark_inode_dirty(new_inode); + } + + if (IS_DIRSYNC(new_dir)) { + err = fat_sync_inode(old_inode); + if (err) + goto error_exchange; + } else { + mark_inode_dirty(old_inode); + } + + /* update ".." directory entry info */ + if (old_dotdot_de) { + fat_set_start(old_dotdot_de, MSDOS_I(new_dir)->i_logstart); + mark_buffer_dirty_inode(old_dotdot_bh, old_inode); + if (IS_DIRSYNC(new_dir)) { + err = sync_dirty_buffer(old_dotdot_bh); + if (err) + goto error_old_dotdot; + } + drop_nlink(old_dir); + inc_nlink(new_dir); + } + + if (new_dotdot_de) { + fat_set_start(new_dotdot_de, MSDOS_I(old_dir)->i_logstart); + mark_buffer_dirty_inode(new_dotdot_bh, new_inode); + if (IS_DIRSYNC(old_dir)) { + err = sync_dirty_buffer(new_dotdot_bh); + if (err) + goto error_new_dotdot; + } + drop_nlink(new_dir); + inc_nlink(old_dir); + } + + /* update inode version and timestamps */ + inode_inc_iversion(old_dir); + inode_inc_iversion(old_inode); + inode_inc_iversion(new_inode); + + fat_truncate_time(old_dir, &ts, S_CTIME | S_MTIME); + + if (IS_DIRSYNC(old_dir)) + (void)fat_sync_inode(old_dir); + else + mark_inode_dirty(old_dir); + + /* if directories are not the same, update new_dir as well */ + if (old_dir != new_dir) { + inode_inc_iversion(new_dir); + fat_truncate_time(new_dir, &ts, S_CTIME | S_MTIME); + + if (IS_DIRSYNC(new_dir)) + (void)fat_sync_inode(new_dir); + else + mark_inode_dirty(new_dir); + } +out: + brelse(old_dotdot_bh); + brelse(new_dotdot_bh); + mutex_unlock(&MSDOS_SB(sb)->s_lock); + + return err; + +error_new_dotdot: + /* data cluster is shared, serious corruption */ + corrupt = 1; + + if (new_dotdot_de) { + fat_set_start(new_dotdot_de, MSDOS_I(new_dir)->i_logstart); + mark_buffer_dirty_inode(new_dotdot_bh, new_inode); + corrupt |= sync_dirty_buffer(new_dotdot_bh); + } + +error_old_dotdot: + /* data cluster is shared, serious corruption */ + corrupt = 1; + + if (old_dotdot_de) { + fat_set_start(old_dotdot_de, MSDOS_I(old_dir)->i_logstart); + mark_buffer_dirty_inode(old_dotdot_bh, old_inode); + corrupt |= sync_dirty_buffer(old_dotdot_bh); + } + +error_exchange: + fat_detach(old_inode); + fat_detach(new_inode); + + fat_attach(old_inode, old_i_pos); + fat_attach(new_inode, new_i_pos); + + if (corrupt) { + corrupt |= fat_sync_inode(old_inode); + corrupt |= fat_sync_inode(new_inode); + } + + if (corrupt < 0) { + fat_fs_error(new_dir->i_sb, + "%s: Filesystem corrupted (i_pos %lld, %lld)", + __func__, old_i_pos, new_i_pos); + } + goto out; +} + static int vfat_rename2(struct user_namespace *mnt_userns, struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry, unsigned int flags) { - if (flags & ~RENAME_NOREPLACE) + if (flags & ~(RENAME_NOREPLACE | RENAME_EXCHANGE)) return -EINVAL; + if (flags & RENAME_EXCHANGE) + return vfat_rename_exchange(old_dir, old_dentry, new_dir, new_dentry); + /* VFS already handled RENAME_NOREPLACE, handle it as a normal rename */ return vfat_rename(old_dir, old_dentry, new_dir, new_dentry); }