Fix several bugs in the FreeBSD rename VOP implementation

- To avoid a use-after-free, zfsvfs->z_log needs to be loaded after the
  teardown lock is acquired with ZFS_ENTER().
- Avoid leaking vnode locks in zfs_rename_relock() and zfs_rename_()
  when the ZFS_ENTER() macros forces an early return.

Refactor the rename implementation so that ZFS_ENTER() can be used
safely.  As a bonus, this lets us use the ZFS_VERIFY_ZP() macro instead
of open-coding its implementation.

Reported-by: Peter Holm <pho@FreeBSD.org>
Tested-by: Peter Holm <pho@FreeBSD.org>
Reviewed-by: Ryan Moeller <ryan@iXsystems.com>
Reviewed-by: Tony Nguyen <tony.nguyen@delphix.com>
Signed-off-by: Mark Johnston <markj@FreeBSD.org>
Sponsored-by: The FreeBSD Foundation
Closes #12717
This commit is contained in:
Mark Johnston 2021-11-19 17:26:39 -05:00 committed by Tony Hutter
parent 47f098d2db
commit 57f43735ca
1 changed files with 135 additions and 137 deletions

View File

@ -2933,48 +2933,18 @@ out2:
} }
/* /*
* We acquire all but fdvp locks using non-blocking acquisitions. If we * Look up the directory entries corresponding to the source and target
* fail to acquire any lock in the path we will drop all held locks, * directory/name pairs.
* acquire the new lock in a blocking fashion, and then release it and
* restart the rename. This acquire/release step ensures that we do not
* spin on a lock waiting for release. On error release all vnode locks
* and decrement references the way tmpfs_rename() would do.
*/ */
static int static int
zfs_rename_relock(struct vnode *sdvp, struct vnode **svpp, zfs_rename_relock_lookup(znode_t *sdzp, const struct componentname *scnp,
struct vnode *tdvp, struct vnode **tvpp, znode_t **szpp, znode_t *tdzp, const struct componentname *tcnp,
const struct componentname *scnp, const struct componentname *tcnp) znode_t **tzpp)
{ {
zfsvfs_t *zfsvfs; zfsvfs_t *zfsvfs;
struct vnode *nvp, *svp, *tvp; znode_t *szp, *tzp;
znode_t *sdzp, *tdzp, *szp, *tzp;
const char *snm = scnp->cn_nameptr;
const char *tnm = tcnp->cn_nameptr;
int error; int error;
VOP_UNLOCK1(tdvp);
if (*tvpp != NULL && *tvpp != tdvp)
VOP_UNLOCK1(*tvpp);
relock:
error = vn_lock(sdvp, LK_EXCLUSIVE);
if (error)
goto out;
sdzp = VTOZ(sdvp);
error = vn_lock(tdvp, LK_EXCLUSIVE | LK_NOWAIT);
if (error != 0) {
VOP_UNLOCK1(sdvp);
if (error != EBUSY)
goto out;
error = vn_lock(tdvp, LK_EXCLUSIVE);
if (error)
goto out;
VOP_UNLOCK1(tdvp);
goto relock;
}
tdzp = VTOZ(tdvp);
/* /*
* Before using sdzp and tdzp we must ensure that they are live. * Before using sdzp and tdzp we must ensure that they are live.
* As a porting legacy from illumos we have two things to worry * As a porting legacy from illumos we have two things to worry
@ -2989,59 +2959,86 @@ relock:
zfsvfs = sdzp->z_zfsvfs; zfsvfs = sdzp->z_zfsvfs;
ASSERT3P(zfsvfs, ==, tdzp->z_zfsvfs); ASSERT3P(zfsvfs, ==, tdzp->z_zfsvfs);
ZFS_ENTER(zfsvfs); ZFS_ENTER(zfsvfs);
ZFS_VERIFY_ZP(sdzp);
/* ZFS_VERIFY_ZP(tdzp);
* We can not use ZFS_VERIFY_ZP() here because it could directly return
* bypassing the cleanup code in the case of an error.
*/
if (tdzp->z_sa_hdl == NULL || sdzp->z_sa_hdl == NULL) {
ZFS_EXIT(zfsvfs);
VOP_UNLOCK1(sdvp);
VOP_UNLOCK1(tdvp);
error = SET_ERROR(EIO);
goto out;
}
/* /*
* Re-resolve svp to be certain it still exists and fetch the * Re-resolve svp to be certain it still exists and fetch the
* correct vnode. * correct vnode.
*/ */
error = zfs_dirent_lookup(sdzp, snm, &szp, ZEXISTS); error = zfs_dirent_lookup(sdzp, scnp->cn_nameptr, &szp, ZEXISTS);
if (error != 0) { if (error != 0) {
/* Source entry invalid or not there. */ /* Source entry invalid or not there. */
ZFS_EXIT(zfsvfs);
VOP_UNLOCK1(sdvp);
VOP_UNLOCK1(tdvp);
if ((scnp->cn_flags & ISDOTDOT) != 0 || if ((scnp->cn_flags & ISDOTDOT) != 0 ||
(scnp->cn_namelen == 1 && scnp->cn_nameptr[0] == '.')) (scnp->cn_namelen == 1 && scnp->cn_nameptr[0] == '.'))
error = SET_ERROR(EINVAL); error = SET_ERROR(EINVAL);
goto out; goto out;
} }
svp = ZTOV(szp); *szpp = szp;
/* /*
* Re-resolve tvp, if it disappeared we just carry on. * Re-resolve tvp, if it disappeared we just carry on.
*/ */
error = zfs_dirent_lookup(tdzp, tnm, &tzp, 0); error = zfs_dirent_lookup(tdzp, tcnp->cn_nameptr, &tzp, 0);
if (error != 0) { if (error != 0) {
ZFS_EXIT(zfsvfs); vrele(ZTOV(szp));
VOP_UNLOCK1(sdvp);
VOP_UNLOCK1(tdvp);
vrele(svp);
if ((tcnp->cn_flags & ISDOTDOT) != 0) if ((tcnp->cn_flags & ISDOTDOT) != 0)
error = SET_ERROR(EINVAL); error = SET_ERROR(EINVAL);
goto out; goto out;
} }
if (tzp != NULL) *tzpp = tzp;
tvp = ZTOV(tzp); out:
else ZFS_EXIT(zfsvfs);
tvp = NULL; return (error);
}
/* /*
* At present the vnode locks must be acquired before z_teardown_lock, * We acquire all but fdvp locks using non-blocking acquisitions. If we
* although it would be more logical to use the opposite order. * fail to acquire any lock in the path we will drop all held locks,
* acquire the new lock in a blocking fashion, and then release it and
* restart the rename. This acquire/release step ensures that we do not
* spin on a lock waiting for release. On error release all vnode locks
* and decrement references the way tmpfs_rename() would do.
*/ */
ZFS_EXIT(zfsvfs); static int
zfs_rename_relock(struct vnode *sdvp, struct vnode **svpp,
struct vnode *tdvp, struct vnode **tvpp,
const struct componentname *scnp, const struct componentname *tcnp)
{
struct vnode *nvp, *svp, *tvp;
znode_t *sdzp, *tdzp, *szp, *tzp;
int error;
VOP_UNLOCK1(tdvp);
if (*tvpp != NULL && *tvpp != tdvp)
VOP_UNLOCK1(*tvpp);
relock:
error = vn_lock(sdvp, LK_EXCLUSIVE);
if (error)
goto out;
error = vn_lock(tdvp, LK_EXCLUSIVE | LK_NOWAIT);
if (error != 0) {
VOP_UNLOCK1(sdvp);
if (error != EBUSY)
goto out;
error = vn_lock(tdvp, LK_EXCLUSIVE);
if (error)
goto out;
VOP_UNLOCK1(tdvp);
goto relock;
}
tdzp = VTOZ(tdvp);
sdzp = VTOZ(sdvp);
error = zfs_rename_relock_lookup(sdzp, scnp, &szp, tdzp, tcnp, &tzp);
if (error != 0) {
VOP_UNLOCK1(sdvp);
VOP_UNLOCK1(tdvp);
goto out;
}
svp = ZTOV(szp);
tvp = tzp != NULL ? ZTOV(tzp) : NULL;
/* /*
* Now try acquire locks on svp and tvp. * Now try acquire locks on svp and tvp.
@ -3178,17 +3175,22 @@ cache_vop_rename(struct vnode *fdvp, struct vnode *fvp, struct vnode *tdvp,
} }
#endif #endif
static int
zfs_do_rename_impl(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
vnode_t *tdvp, vnode_t **tvpp, struct componentname *tcnp,
cred_t *cr);
/* /*
* Move an entry from the provided source directory to the target * Move an entry from the provided source directory to the target
* directory. Change the entry name as indicated. * directory. Change the entry name as indicated.
* *
* IN: sdvp - Source directory containing the "old entry". * IN: sdvp - Source directory containing the "old entry".
* snm - Old entry name. * scnp - Old entry name.
* tdvp - Target directory to contain the "new entry". * tdvp - Target directory to contain the "new entry".
* tnm - New entry name. * tcnp - New entry name.
* cr - credentials of caller. * cr - credentials of caller.
* ct - caller context * INOUT: svpp - Source file
* flags - case flags * tvpp - Target file, may point to NULL initially
* *
* RETURN: 0 on success, error code on failure. * RETURN: 0 on success, error code on failure.
* *
@ -3197,18 +3199,15 @@ cache_vop_rename(struct vnode *fdvp, struct vnode *fvp, struct vnode *tdvp,
*/ */
/*ARGSUSED*/ /*ARGSUSED*/
static int static int
zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp, zfs_do_rename(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
vnode_t *tdvp, vnode_t **tvpp, struct componentname *tcnp, vnode_t *tdvp, vnode_t **tvpp, struct componentname *tcnp,
cred_t *cr, int log) cred_t *cr)
{ {
zfsvfs_t *zfsvfs; int error;
znode_t *sdzp, *tdzp, *szp, *tzp;
zilog_t *zilog = NULL; ASSERT_VOP_ELOCKED(tdvp, __func__);
dmu_tx_t *tx; if (*tvpp != NULL)
const char *snm = scnp->cn_nameptr; ASSERT_VOP_ELOCKED(*tvpp, __func__);
const char *tnm = tcnp->cn_nameptr;
int error = 0;
bool want_seqc_end __maybe_unused = false;
/* Reject renames across filesystems. */ /* Reject renames across filesystems. */
if ((*svpp)->v_mount != tdvp->v_mount || if ((*svpp)->v_mount != tdvp->v_mount ||
@ -3231,51 +3230,64 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
return (error); return (error);
} }
error = zfs_do_rename_impl(sdvp, svpp, scnp, tdvp, tvpp, tcnp, cr);
VOP_UNLOCK1(sdvp);
VOP_UNLOCK1(*svpp);
out:
if (*tvpp != NULL)
VOP_UNLOCK1(*tvpp);
if (tdvp != *tvpp)
VOP_UNLOCK1(tdvp);
return (error);
}
static int
zfs_do_rename_impl(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
vnode_t *tdvp, vnode_t **tvpp, struct componentname *tcnp,
cred_t *cr)
{
dmu_tx_t *tx;
zfsvfs_t *zfsvfs;
zilog_t *zilog;
znode_t *tdzp, *sdzp, *tzp, *szp;
const char *snm = scnp->cn_nameptr;
const char *tnm = tcnp->cn_nameptr;
int error;
tdzp = VTOZ(tdvp); tdzp = VTOZ(tdvp);
sdzp = VTOZ(sdvp); sdzp = VTOZ(sdvp);
zfsvfs = tdzp->z_zfsvfs; zfsvfs = tdzp->z_zfsvfs;
zilog = zfsvfs->z_log;
/*
* After we re-enter ZFS_ENTER() we will have to revalidate all
* znodes involved.
*/
ZFS_ENTER(zfsvfs); ZFS_ENTER(zfsvfs);
ZFS_VERIFY_ZP(tdzp);
ZFS_VERIFY_ZP(sdzp);
zilog = zfsvfs->z_log;
if (zfsvfs->z_utf8 && u8_validate(tnm, if (zfsvfs->z_utf8 && u8_validate(tnm,
strlen(tnm), NULL, U8_VALIDATE_ENTIRE, &error) < 0) { strlen(tnm), NULL, U8_VALIDATE_ENTIRE, &error) < 0) {
error = SET_ERROR(EILSEQ); error = SET_ERROR(EILSEQ);
goto unlockout; goto out;
} }
/* If source and target are the same file, there is nothing to do. */ /* If source and target are the same file, there is nothing to do. */
if ((*svpp) == (*tvpp)) { if ((*svpp) == (*tvpp)) {
error = 0; error = 0;
goto unlockout; goto out;
} }
if (((*svpp)->v_type == VDIR && (*svpp)->v_mountedhere != NULL) || if (((*svpp)->v_type == VDIR && (*svpp)->v_mountedhere != NULL) ||
((*tvpp) != NULL && (*tvpp)->v_type == VDIR && ((*tvpp) != NULL && (*tvpp)->v_type == VDIR &&
(*tvpp)->v_mountedhere != NULL)) { (*tvpp)->v_mountedhere != NULL)) {
error = SET_ERROR(EXDEV); error = SET_ERROR(EXDEV);
goto unlockout; goto out;
}
/*
* We can not use ZFS_VERIFY_ZP() here because it could directly return
* bypassing the cleanup code in the case of an error.
*/
if (tdzp->z_sa_hdl == NULL || sdzp->z_sa_hdl == NULL) {
error = SET_ERROR(EIO);
goto unlockout;
} }
szp = VTOZ(*svpp); szp = VTOZ(*svpp);
ZFS_VERIFY_ZP(szp);
tzp = *tvpp == NULL ? NULL : VTOZ(*tvpp); tzp = *tvpp == NULL ? NULL : VTOZ(*tvpp);
if (szp->z_sa_hdl == NULL || (tzp != NULL && tzp->z_sa_hdl == NULL)) { if (tzp != NULL)
error = SET_ERROR(EIO); ZFS_VERIFY_ZP(tzp);
goto unlockout;
}
/* /*
* This is to prevent the creation of links into attribute space * This is to prevent the creation of links into attribute space
@ -3284,7 +3296,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
*/ */
if ((tdzp->z_pflags & ZFS_XATTR) != (sdzp->z_pflags & ZFS_XATTR)) { if ((tdzp->z_pflags & ZFS_XATTR) != (sdzp->z_pflags & ZFS_XATTR)) {
error = SET_ERROR(EINVAL); error = SET_ERROR(EINVAL);
goto unlockout; goto out;
} }
/* /*
@ -3297,7 +3309,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
if (tdzp->z_pflags & ZFS_PROJINHERIT && if (tdzp->z_pflags & ZFS_PROJINHERIT &&
tdzp->z_projid != szp->z_projid) { tdzp->z_projid != szp->z_projid) {
error = SET_ERROR(EXDEV); error = SET_ERROR(EXDEV);
goto unlockout; goto out;
} }
/* /*
@ -3307,7 +3319,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
* done in a single check. * done in a single check.
*/ */
if ((error = zfs_zaccess_rename(sdzp, szp, tdzp, tzp, cr))) if ((error = zfs_zaccess_rename(sdzp, szp, tdzp, tzp, cr)))
goto unlockout; goto out;
if ((*svpp)->v_type == VDIR) { if ((*svpp)->v_type == VDIR) {
/* /*
@ -3317,7 +3329,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
sdzp == szp || sdzp == szp ||
(scnp->cn_flags | tcnp->cn_flags) & ISDOTDOT) { (scnp->cn_flags | tcnp->cn_flags) & ISDOTDOT) {
error = EINVAL; error = EINVAL;
goto unlockout; goto out;
} }
/* /*
@ -3325,7 +3337,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
* Can't do a move like this: /usr/a/b to /usr/a/b/c/d * Can't do a move like this: /usr/a/b to /usr/a/b/c/d
*/ */
if ((error = zfs_rename_check(szp, sdzp, tdzp))) if ((error = zfs_rename_check(szp, sdzp, tdzp)))
goto unlockout; goto out;
} }
/* /*
@ -3338,7 +3350,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
if ((*svpp)->v_type == VDIR) { if ((*svpp)->v_type == VDIR) {
if ((*tvpp)->v_type != VDIR) { if ((*tvpp)->v_type != VDIR) {
error = SET_ERROR(ENOTDIR); error = SET_ERROR(ENOTDIR);
goto unlockout; goto out;
} else { } else {
cache_purge(tdvp); cache_purge(tdvp);
if (sdvp != tdvp) if (sdvp != tdvp)
@ -3347,7 +3359,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
} else { } else {
if ((*tvpp)->v_type == VDIR) { if ((*tvpp)->v_type == VDIR) {
error = SET_ERROR(EISDIR); error = SET_ERROR(EISDIR);
goto unlockout; goto out;
} }
} }
} }
@ -3358,9 +3370,7 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
vn_seqc_write_begin(*tvpp); vn_seqc_write_begin(*tvpp);
if (tdvp != *tvpp) if (tdvp != *tvpp)
vn_seqc_write_begin(tdvp); vn_seqc_write_begin(tdvp);
#if __FreeBSD_version >= 1300102
want_seqc_end = true;
#endif
vnevent_rename_src(*svpp, sdvp, scnp->cn_nameptr, ct); vnevent_rename_src(*svpp, sdvp, scnp->cn_nameptr, ct);
if (tzp) if (tzp)
vnevent_rename_dest(*tvpp, tdvp, tnm, ct); vnevent_rename_dest(*tvpp, tdvp, tnm, ct);
@ -3392,10 +3402,9 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
error = dmu_tx_assign(tx, TXG_WAIT); error = dmu_tx_assign(tx, TXG_WAIT);
if (error) { if (error) {
dmu_tx_abort(tx); dmu_tx_abort(tx);
goto unlockout; goto out_seq;
} }
if (tzp) /* Attempt to remove the existing target */ if (tzp) /* Attempt to remove the existing target */
error = zfs_link_destroy(tdzp, tnm, tzp, tx, 0, NULL); error = zfs_link_destroy(tdzp, tnm, tzp, tx, 0, NULL);
@ -3442,30 +3451,19 @@ zfs_rename_(vnode_t *sdvp, vnode_t **svpp, struct componentname *scnp,
dmu_tx_commit(tx); dmu_tx_commit(tx);
unlockout: /* all 4 vnodes are locked, ZFS_ENTER called */ out_seq:
if (want_seqc_end) {
vn_seqc_write_end(*svpp); vn_seqc_write_end(*svpp);
vn_seqc_write_end(sdvp); vn_seqc_write_end(sdvp);
if (*tvpp != NULL) if (*tvpp != NULL)
vn_seqc_write_end(*tvpp); vn_seqc_write_end(*tvpp);
if (tdvp != *tvpp) if (tdvp != *tvpp)
vn_seqc_write_end(tdvp); vn_seqc_write_end(tdvp);
want_seqc_end = false;
}
VOP_UNLOCK1(*svpp);
VOP_UNLOCK1(sdvp);
out:
if (error == 0 && zfsvfs->z_os->os_sync == ZFS_SYNC_ALWAYS) if (error == 0 && zfsvfs->z_os->os_sync == ZFS_SYNC_ALWAYS)
zil_commit(zilog, 0); zil_commit(zilog, 0);
ZFS_EXIT(zfsvfs); ZFS_EXIT(zfsvfs);
out: /* original two vnodes are locked */
MPASS(!want_seqc_end);
if (*tvpp != NULL)
VOP_UNLOCK1(*tvpp);
if (tdvp != *tvpp)
VOP_UNLOCK1(tdvp);
return (error); return (error);
} }
@ -3497,7 +3495,7 @@ zfs_rename(znode_t *sdzp, const char *sname, znode_t *tdzp, const char *tname,
goto fail; goto fail;
} }
error = zfs_rename_(sdvp, &svp, &scn, tdvp, &tvp, &tcn, cr, 0); error = zfs_do_rename(sdvp, &svp, &scn, tdvp, &tvp, &tcn, cr);
fail: fail:
if (svp != NULL) if (svp != NULL)
vrele(svp); vrele(svp);
@ -4955,8 +4953,8 @@ zfs_freebsd_rename(struct vop_rename_args *ap)
ASSERT(ap->a_fcnp->cn_flags & (SAVENAME|SAVESTART)); ASSERT(ap->a_fcnp->cn_flags & (SAVENAME|SAVESTART));
ASSERT(ap->a_tcnp->cn_flags & (SAVENAME|SAVESTART)); ASSERT(ap->a_tcnp->cn_flags & (SAVENAME|SAVESTART));
error = zfs_rename_(fdvp, &fvp, ap->a_fcnp, tdvp, &tvp, error = zfs_do_rename(fdvp, &fvp, ap->a_fcnp, tdvp, &tvp,
ap->a_tcnp, ap->a_fcnp->cn_cred, 1); ap->a_tcnp, ap->a_fcnp->cn_cred);
vrele(fdvp); vrele(fdvp);
vrele(fvp); vrele(fvp);