Currently rename() returns EXDEV when source and destination are on different mounts, even if they're bind mounts of the same filesystem. This forces userspace (mv) to fall back to slow copy+delete. Change the check from comparing mount pointers to comparing superblocks, which allows renaming across bind mounts within the same filesystem. This also handles the case where source and destination mounts have different properties (idmaps, read-only flags) by checking both mounts appropriately. Signed-off-by: Andrei Topala --- fs/cachefiles/namei.c | 3 ++- fs/ecryptfs/inode.c | 3 ++- fs/namei.c | 39 +++++++++++++++++++++++++-------------- fs/nfsd/vfs.c | 3 ++- fs/overlayfs/copy_up.c | 6 ++++-- fs/overlayfs/dir.c | 12 ++++++++---- fs/overlayfs/overlayfs.h | 3 ++- fs/overlayfs/super.c | 3 ++- fs/smb/server/vfs.c | 3 ++- include/linux/fs.h | 6 ++++-- 10 files changed, 53 insertions(+), 28 deletions(-) diff --git a/fs/cachefiles/namei.c b/fs/cachefiles/namei.c index e5ec90dcc..771f169cc 100644 --- a/fs/cachefiles/namei.c +++ b/fs/cachefiles/namei.c @@ -383,7 +383,8 @@ int cachefiles_bury_object(struct cachefiles_cache *cache, cachefiles_io_error(cache, "Rename security error %d", ret); } else { struct renamedata rd = { - .mnt_idmap = &nop_mnt_idmap, + .old_mnt_idmap = &nop_mnt_idmap, + .new_mnt_idmap = &nop_mnt_idmap, .old_parent = dir, .old_dentry = rep, .new_parent = cache->graveyard, diff --git a/fs/ecryptfs/inode.c b/fs/ecryptfs/inode.c index 397824824..1d59b8a2e 100644 --- a/fs/ecryptfs/inode.c +++ b/fs/ecryptfs/inode.c @@ -614,7 +614,8 @@ ecryptfs_rename(struct mnt_idmap *idmap, struct inode *old_dir, target_inode = d_inode(new_dentry); - rd.mnt_idmap = &nop_mnt_idmap; + rd.old_mnt_idmap = &nop_mnt_idmap; + rd.new_mnt_idmap = &nop_mnt_idmap; rd.old_parent = lower_old_dir_dentry; rd.new_parent = lower_new_dir_dentry; rc = start_renaming_two_dentries(&rd, lower_old_dentry, lower_new_dentry); diff --git a/fs/namei.c b/fs/namei.c index bf0f66f0e..be9931d63 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -3867,7 +3867,8 @@ __start_renaming(struct renamedata *rd, int lookup_flags, * These references and the lock are dropped by end_renaming(). * * The passed in qstrs need not have the hash calculated, and basic - * eXecute permission checking is performed against @rd.mnt_idmap. + * eXecute permission checking is performed against @rd.old_mnt_idmap + * and @rd.new_mnt_idmap respectively. * * Returns: zero or an error. */ @@ -3876,10 +3877,10 @@ int start_renaming(struct renamedata *rd, int lookup_flags, { int err; - err = lookup_one_common(rd->mnt_idmap, old_last, rd->old_parent); + err = lookup_one_common(rd->old_mnt_idmap, old_last, rd->old_parent); if (err) return err; - err = lookup_one_common(rd->mnt_idmap, new_last, rd->new_parent); + err = lookup_one_common(rd->new_mnt_idmap, new_last, rd->new_parent); if (err) return err; return __start_renaming(rd, lookup_flags, old_last, new_last); @@ -3964,7 +3965,7 @@ __start_renaming_dentry(struct renamedata *rd, int lookup_flags, * References and the lock can be dropped with end_renaming() * * The passed in qstr need not have the hash calculated, and basic - * eXecute permission checking is performed against @rd.mnt_idmap. + * eXecute permission checking is performed against @rd.new_mnt_idmap. * * Returns: zero or an error. */ @@ -3973,7 +3974,7 @@ int start_renaming_dentry(struct renamedata *rd, int lookup_flags, { int err; - err = lookup_one_common(rd->mnt_idmap, new_last, rd->new_parent); + err = lookup_one_common(rd->new_mnt_idmap, new_last, rd->new_parent); if (err) return err; return __start_renaming_dentry(rd, lookup_flags, old_dentry, new_last); @@ -5816,20 +5817,20 @@ int vfs_rename(struct renamedata *rd) if (source == target) return 0; - error = may_delete(rd->mnt_idmap, old_dir, old_dentry, is_dir); + error = may_delete(rd->old_mnt_idmap, old_dir, old_dentry, is_dir); if (error) return error; if (!target) { - error = may_create(rd->mnt_idmap, new_dir, new_dentry); + error = may_create(rd->new_mnt_idmap, new_dir, new_dentry); } else { new_is_dir = d_is_dir(new_dentry); if (!(flags & RENAME_EXCHANGE)) - error = may_delete(rd->mnt_idmap, new_dir, + error = may_delete(rd->new_mnt_idmap, new_dir, new_dentry, is_dir); else - error = may_delete(rd->mnt_idmap, new_dir, + error = may_delete(rd->new_mnt_idmap, new_dir, new_dentry, new_is_dir); } if (error) @@ -5844,13 +5845,13 @@ int vfs_rename(struct renamedata *rd) */ if (new_dir != old_dir) { if (is_dir) { - error = inode_permission(rd->mnt_idmap, source, + error = inode_permission(rd->old_mnt_idmap, source, MAY_WRITE); if (error) return error; } if ((flags & RENAME_EXCHANGE) && new_is_dir) { - error = inode_permission(rd->mnt_idmap, target, + error = inode_permission(rd->new_mnt_idmap, target, MAY_WRITE); if (error) return error; @@ -5926,7 +5927,7 @@ int vfs_rename(struct renamedata *rd) if (error) goto out; } - error = old_dir->i_op->rename(rd->mnt_idmap, old_dir, old_dentry, + error = old_dir->i_op->rename(rd->old_mnt_idmap, old_dir, old_dentry, new_dir, new_dentry, flags); if (error) goto out; @@ -5996,7 +5997,7 @@ int do_renameat2(int olddfd, struct filename *from, int newdfd, goto exit1; error = -EXDEV; - if (old_path.mnt != new_path.mnt) + if (old_path.mnt->mnt_sb != new_path.mnt->mnt_sb) goto exit2; error = -EBUSY; @@ -6012,9 +6013,16 @@ int do_renameat2(int olddfd, struct filename *from, int newdfd, if (error) goto exit2; + if (old_path.mnt != new_path.mnt) { + error = mnt_want_write(new_path.mnt); + if (error) + goto exit3; + } + retry_deleg: rd.old_parent = old_path.dentry; - rd.mnt_idmap = mnt_idmap(old_path.mnt); + rd.old_mnt_idmap = mnt_idmap(old_path.mnt); + rd.new_mnt_idmap = mnt_idmap(new_path.mnt); rd.new_parent = new_path.dentry; rd.delegated_inode = &delegated_inode; rd.flags = flags; @@ -6053,6 +6061,9 @@ int do_renameat2(int olddfd, struct filename *from, int newdfd, if (!error) goto retry_deleg; } + if (old_path.mnt != new_path.mnt) + mnt_drop_write(new_path.mnt); +exit3: mnt_drop_write(old_path.mnt); exit2: if (retry_estale(error, lookup_flags)) diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c index 964cf922a..5ffcd279f 100644 --- a/fs/nfsd/vfs.c +++ b/fs/nfsd/vfs.c @@ -2154,7 +2154,8 @@ nfsd_rename(struct svc_rqst *rqstp, struct svc_fh *ffhp, char *fname, int flen, goto out; } - rd.mnt_idmap = &nop_mnt_idmap; + rd.old_mnt_idmap = &nop_mnt_idmap; + rd.new_mnt_idmap = &nop_mnt_idmap; rd.old_parent = fdentry; rd.new_parent = tdentry; diff --git a/fs/overlayfs/copy_up.c b/fs/overlayfs/copy_up.c index 758611ee4..0f1df4fef 100644 --- a/fs/overlayfs/copy_up.c +++ b/fs/overlayfs/copy_up.c @@ -556,7 +556,8 @@ static int ovl_create_index(struct dentry *dentry, const struct ovl_fh *fh, if (err) goto out; - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = indexdir; rd.new_parent = indexdir; err = start_renaming_dentry(&rd, 0, temp, &name); @@ -804,7 +805,8 @@ static int ovl_copy_up_workdir(struct ovl_copy_up_ctx *c) * ovl_copy_up_data(), so lock workdir and destdir and make sure that * temp wasn't moved before copy up completion or cleanup. */ - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = c->workdir; rd.new_parent = c->destdir; rd.flags = 0; diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c index ff3dbd1ca..cb988dfa3 100644 --- a/fs/overlayfs/dir.c +++ b/fs/overlayfs/dir.c @@ -135,7 +135,8 @@ int ovl_cleanup_and_whiteout(struct ovl_fs *ofs, struct dentry *dir, if (d_is_dir(dentry)) flags = RENAME_EXCHANGE; - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = ofs->workdir; rd.new_parent = dir; rd.flags = flags; @@ -413,7 +414,8 @@ static struct dentry *ovl_clear_empty(struct dentry *dentry, if (IS_ERR(opaquedir)) goto out; - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = workdir; rd.new_parent = upperdir; rd.flags = RENAME_EXCHANGE; @@ -504,7 +506,8 @@ static int ovl_create_over_whiteout(struct dentry *dentry, struct inode *inode, if (IS_ERR(newdentry)) goto out_dput; - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = workdir; rd.new_parent = upperdir; rd.flags = 0; @@ -1230,7 +1233,8 @@ static int ovl_rename_upper(struct ovl_renamedata *ovlrd, struct list_head *list } } - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = old_upperdir; rd.new_parent = new_upperdir; rd.flags = ovlrd->flags; diff --git a/fs/overlayfs/overlayfs.h b/fs/overlayfs/overlayfs.h index f9ac9bdde..4b7824761 100644 --- a/fs/overlayfs/overlayfs.h +++ b/fs/overlayfs/overlayfs.h @@ -374,7 +374,8 @@ static inline int ovl_do_rename(struct ovl_fs *ofs, struct dentry *olddir, struct dentry *newdentry, unsigned int flags) { struct renamedata rd = { - .mnt_idmap = ovl_upper_mnt_idmap(ofs), + .old_mnt_idmap = ovl_upper_mnt_idmap(ofs), + .new_mnt_idmap = ovl_upper_mnt_idmap(ofs), .old_parent = olddir, .old_dentry = olddentry, .new_parent = newdir, diff --git a/fs/overlayfs/super.c b/fs/overlayfs/super.c index ba9146f22..86d164c05 100644 --- a/fs/overlayfs/super.c +++ b/fs/overlayfs/super.c @@ -582,7 +582,8 @@ static int ovl_check_rename_whiteout(struct ovl_fs *ofs) if (IS_ERR(temp)) return err; - rd.mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.old_mnt_idmap = ovl_upper_mnt_idmap(ofs); + rd.new_mnt_idmap = ovl_upper_mnt_idmap(ofs); rd.old_parent = workdir; rd.new_parent = workdir; rd.flags = RENAME_WHITEOUT; diff --git a/fs/smb/server/vfs.c b/fs/smb/server/vfs.c index f891344bd..f9e46b6e0 100644 --- a/fs/smb/server/vfs.c +++ b/fs/smb/server/vfs.c @@ -698,7 +698,8 @@ int ksmbd_vfs_rename(struct ksmbd_work *work, const struct path *old_path, if (err) goto out2; - rd.mnt_idmap = mnt_idmap(old_path->mnt); + rd.old_mnt_idmap = mnt_idmap(old_path->mnt); + rd.new_mnt_idmap = mnt_idmap(new_path.mnt); rd.old_parent = NULL; rd.new_parent = new_path.dentry; rd.flags = flags; diff --git a/include/linux/fs.h b/include/linux/fs.h index f5c9cf28c..4b7a1fd23 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -1774,7 +1774,8 @@ int vfs_unlink(struct mnt_idmap *, struct inode *, struct dentry *, /** * struct renamedata - contains all information required for renaming - * @mnt_idmap: idmap of the mount in which the rename is happening. + * @old_mnt_idmap: idmap of the source mount + * @new_mnt_idmap: idmap of the destination mount * @old_parent: parent of source * @old_dentry: source * @new_parent: parent of destination @@ -1783,7 +1784,8 @@ int vfs_unlink(struct mnt_idmap *, struct inode *, struct dentry *, * @flags: rename flags */ struct renamedata { - struct mnt_idmap *mnt_idmap; + struct mnt_idmap *old_mnt_idmap; + struct mnt_idmap *new_mnt_idmap; struct dentry *old_parent; struct dentry *old_dentry; struct dentry *new_parent; -- 2.50.1