diff --git a/lib/libsys/rename.2 b/lib/libsys/rename.2 --- a/lib/libsys/rename.2 +++ b/lib/libsys/rename.2 @@ -143,6 +143,12 @@ .Fa to exists, the request fails with the error .Er EEXIST . +.It Dv AT_RENAME_EXCHANGE +Atomically exchange the files pointed to by the +.Fa from +and +.Fa to +names. .El .Sh RETURN VALUES .Rv -std rename diff --git a/sys/fs/tmpfs/tmpfs_vnops.c b/sys/fs/tmpfs/tmpfs_vnops.c --- a/sys/fs/tmpfs/tmpfs_vnops.c +++ b/sys/fs/tmpfs/tmpfs_vnops.c @@ -962,6 +962,89 @@ return (error); } +static int +tmpfs_rename_check_parent(struct tmpfs_mount *tmp, struct tmpfs_node *fdnode, + struct vnode *fvp, struct tmpfs_node *fnode, struct tmpfs_dirent *de, + struct tmpfs_node *tdnode, struct ucred *tcred) +{ + struct tmpfs_node *n; + int error; + + TMPFS_NODE_LOCK(fnode); + error = tmpfs_access_locked(fvp, fnode, VWRITE, tcred); + TMPFS_NODE_UNLOCK(fnode); + if (error != 0) + return (error); + + /* + * Ensure the target directory is not a child of the + * directory being moved. Otherwise, we'd end up + * with stale nodes. + * + * TMPFS_LOCK guaranties that no nodes are freed while + * traversing the list. Nodes can only be marked as + * removed: tn_parent == NULL. + */ + n = tdnode; + TMPFS_LOCK(tmp); + TMPFS_NODE_LOCK(n); + while (n != n->tn_dir.tn_parent) { + struct tmpfs_node *parent; + + if (n == fnode) { + TMPFS_NODE_UNLOCK(n); + TMPFS_UNLOCK(tmp); + return (EINVAL); + } + parent = n->tn_dir.tn_parent; + TMPFS_NODE_UNLOCK(n); + if (parent == NULL) { + n = NULL; + break; + } + TMPFS_NODE_LOCK(parent); + if (parent->tn_dir.tn_parent == NULL) { + TMPFS_NODE_UNLOCK(parent); + n = NULL; + break; + } + n = parent; + } + TMPFS_UNLOCK(tmp); + if (n == NULL) + return (EINVAL); + + TMPFS_NODE_UNLOCK(n); + + return (0); +} + +static void +tmpfs_rename_set_parent(struct tmpfs_node *fdnode, struct tmpfs_node *fnode, + struct tmpfs_dirent *de, struct tmpfs_node *tdnode) +{ + /* Adjust the parent pointer. */ + TMPFS_VALIDATE_DIR(fnode); + TMPFS_NODE_LOCK(de->td_node); + de->td_node->tn_dir.tn_parent = tdnode; + TMPFS_NODE_UNLOCK(de->td_node); + + /* + * As a result of changing the target of the '..' + * entry, the link count of the source and target + * directories has to be adjusted. + */ + TMPFS_NODE_LOCK(tdnode); + TMPFS_ASSERT_LOCKED(tdnode); + tdnode->tn_links++; + TMPFS_NODE_UNLOCK(tdnode); + + TMPFS_NODE_LOCK(fdnode); + TMPFS_ASSERT_LOCKED(fdnode); + fdnode->tn_links--; + TMPFS_NODE_UNLOCK(fdnode); +} + static int tmpfs_rename(struct vop_rename_args *v) { @@ -972,7 +1055,7 @@ struct vnode *tvp = v->a_tvp; struct componentname *tcnp = v->a_tcnp; char *newname; - struct tmpfs_dirent *de; + struct tmpfs_dirent *de, *tde; struct tmpfs_mount *tmp; struct tmpfs_node *fdnode; struct tmpfs_node *fnode; @@ -980,8 +1063,11 @@ struct tmpfs_node *tdnode; int error; bool want_seqc_end; + bool exchange; want_seqc_end = false; + newname = NULL; + error = 0; /* * Disallow cross-device renames. @@ -993,16 +1079,15 @@ goto out; } - if ((v->a_flags & ~(AT_RENAME_NOREPLACE)) != 0) { + if ((v->a_flags & ~(AT_RENAME_NOREPLACE | AT_RENAME_EXCHANGE)) != 0) { error = EOPNOTSUPP; goto out; } /* If source and target are the same file, there is nothing to do. */ - if (fvp == tvp) { - error = 0; + if (fvp == tvp) goto out; - } + exchange = (v->a_flags & AT_RENAME_EXCHANGE) != 0; /* * If we need to move the directory between entries, lock the @@ -1055,6 +1140,8 @@ fdnode = VP_TO_TMPFS_DIR(fdvp); fnode = VP_TO_TMPFS_NODE(fvp); de = tmpfs_dir_lookup(fdnode, fnode, fcnp); + if (exchange) + tde = tmpfs_dir_lookup(tdnode, tnode, tcnp); /* * Entry can disappear before we lock fdvp. @@ -1068,6 +1155,13 @@ goto out_locked; } MPASS(de->td_node == fnode); + if (exchange) { + if (tde == NULL) { + error = ENOENT; + goto out_locked; + } + MPASS(tde->td_node == tnode); + } /* * If re-naming a directory to another preexisting directory @@ -1085,27 +1179,32 @@ goto out_locked; } - if (fnode->tn_type == VDIR && tnode->tn_type == VDIR) { - if (tnode->tn_size != 0 && - ((tcnp->cn_flags & IGNOREWHITEOUT) == 0 || - tnode->tn_size > tnode->tn_dir.tn_wht_size)) { - error = ENOTEMPTY; + if (!exchange) { + if (fnode->tn_type == VDIR && tnode->tn_type == VDIR) { + if (tnode->tn_size != 0 && + ((tcnp->cn_flags & IGNOREWHITEOUT) == 0 || + tnode->tn_size > + tnode->tn_dir.tn_wht_size)) { + error = ENOTEMPTY; + goto out_locked; + } + } else if (fnode->tn_type == VDIR && + tnode->tn_type != VDIR) { + error = ENOTDIR; goto out_locked; + } else if (fnode->tn_type != VDIR && + tnode->tn_type == VDIR) { + error = EISDIR; + goto out_locked; + } else { + MPASS(fnode->tn_type != VDIR && + tnode->tn_type != VDIR); } - } else if (fnode->tn_type == VDIR && tnode->tn_type != VDIR) { - error = ENOTDIR; - goto out_locked; - } else if (fnode->tn_type != VDIR && tnode->tn_type == VDIR) { - error = EISDIR; - goto out_locked; - } else { - MPASS(fnode->tn_type != VDIR && - tnode->tn_type != VDIR); } } - if ((fnode->tn_flags & (NOUNLINK | IMMUTABLE | APPEND)) - || (fdnode->tn_flags & (APPEND | IMMUTABLE))) { + if ((fnode->tn_flags & (NOUNLINK | IMMUTABLE | APPEND)) != 0 || + (fdnode->tn_flags & (APPEND | IMMUTABLE)) != 0) { error = EPERM; goto out_locked; } @@ -1114,11 +1213,9 @@ * Ensure that we have enough memory to hold the new name, if it * has to be changed. */ - if (fcnp->cn_namelen != tcnp->cn_namelen || - bcmp(fcnp->cn_nameptr, tcnp->cn_nameptr, fcnp->cn_namelen) != 0) { + if (!exchange && (fcnp->cn_namelen != tcnp->cn_namelen || + bcmp(fcnp->cn_nameptr, tcnp->cn_nameptr, fcnp->cn_namelen) != 0)) newname = malloc(tcnp->cn_namelen, M_TMPFSNAME, M_WAITOK); - } else - newname = NULL; /* * If the node is being moved to another directory, we have to do @@ -1129,87 +1226,30 @@ * In case we are moving a directory, we have to adjust its * parent to point to the new parent. */ - if (de->td_node->tn_type == VDIR) { - struct tmpfs_node *n; - - TMPFS_NODE_LOCK(fnode); - error = tmpfs_access_locked(fvp, fnode, VWRITE, - tcnp->cn_cred); - TMPFS_NODE_UNLOCK(fnode); - if (error) { - if (newname != NULL) - free(newname, M_TMPFSNAME); + if (fnode->tn_type == VDIR) { + error = tmpfs_rename_check_parent(tmp, fdnode, fvp, + fnode, de, tdnode, tcnp->cn_cred); + if (error != 0) { + free(newname, M_TMPFSNAME); goto out_locked; } - - /* - * Ensure the target directory is not a child of the - * directory being moved. Otherwise, we'd end up - * with stale nodes. - */ - n = tdnode; - /* - * TMPFS_LOCK guaranties that no nodes are freed while - * traversing the list. Nodes can only be marked as - * removed: tn_parent == NULL. - */ - TMPFS_LOCK(tmp); - TMPFS_NODE_LOCK(n); - while (n != n->tn_dir.tn_parent) { - struct tmpfs_node *parent; - - if (n == fnode) { - TMPFS_NODE_UNLOCK(n); - TMPFS_UNLOCK(tmp); - error = EINVAL; - if (newname != NULL) - free(newname, M_TMPFSNAME); - goto out_locked; - } - parent = n->tn_dir.tn_parent; - TMPFS_NODE_UNLOCK(n); - if (parent == NULL) { - n = NULL; - break; - } - TMPFS_NODE_LOCK(parent); - if (parent->tn_dir.tn_parent == NULL) { - TMPFS_NODE_UNLOCK(parent); - n = NULL; - break; - } - n = parent; - } - TMPFS_UNLOCK(tmp); - if (n == NULL) { - error = EINVAL; - if (newname != NULL) - free(newname, M_TMPFSNAME); - goto out_locked; - } - TMPFS_NODE_UNLOCK(n); - - /* Adjust the parent pointer. */ - TMPFS_VALIDATE_DIR(fnode); - TMPFS_NODE_LOCK(de->td_node); - de->td_node->tn_dir.tn_parent = tdnode; - TMPFS_NODE_UNLOCK(de->td_node); - - /* - * As a result of changing the target of the '..' - * entry, the link count of the source and target - * directories has to be adjusted. - */ - TMPFS_NODE_LOCK(tdnode); - TMPFS_ASSERT_LOCKED(tdnode); - tdnode->tn_links++; - TMPFS_NODE_UNLOCK(tdnode); - - TMPFS_NODE_LOCK(fdnode); - TMPFS_ASSERT_LOCKED(fdnode); - fdnode->tn_links--; - TMPFS_NODE_UNLOCK(fdnode); } + if (exchange && tnode->tn_type == VDIR) { + MPASS(newname == NULL); + error = tmpfs_rename_check_parent(tmp, tdnode, tvp, + tnode, tde, fdnode, fcnp->cn_cred); + if (error != 0) + goto out_locked; + } + if (fnode->tn_type == VDIR) + tmpfs_rename_set_parent(fdnode, fnode, de, tdnode); + if (exchange && tnode->tn_type == VDIR) + tmpfs_rename_set_parent(tdnode, tnode, tde, fdnode); + } + if (exchange) { + de->td_node = tnode; + tde->td_node = fnode; + goto out_locked; } /* diff --git a/sys/kern/vfs_syscalls.c b/sys/kern/vfs_syscalls.c --- a/sys/kern/vfs_syscalls.c +++ b/sys/kern/vfs_syscalls.c @@ -3786,7 +3786,7 @@ int error; short irflag; - if ((flags & ~(AT_RENAME_NOREPLACE)) != 0) + if ((flags & ~(AT_RENAME_NOREPLACE | AT_RENAME_EXCHANGE)) != 0) return (EINVAL); again: tmp = mp = NULL; @@ -3799,7 +3799,8 @@ return (error); } else { #endif - NDINIT_ATRIGHTS(&fromnd, DELETE, WANTPARENT | AUDITVNODE1, + NDINIT_ATRIGHTS(&fromnd, (flags & AT_RENAME_EXCHANGE) != 0 ? RENAME : + DELETE, WANTPARENT | AUDITVNODE1, pathseg, old, oldfd, &cap_renameat_source_rights); if ((error = namei(&fromnd)) != 0) return (error); @@ -3845,6 +3846,10 @@ error = EEXIST; goto out; } + if (tvp == NULL && (flags & AT_RENAME_EXCHANGE) != 0) { + error = ENOENT; + goto out; + } error = vn_start_write(fvp, &mp, V_NOWAIT); if (error != 0) { again1: @@ -3887,12 +3892,15 @@ goto out; } if (tvp != NULL) { - if (fvp->v_type == VDIR && tvp->v_type != VDIR) { - error = ENOTDIR; - goto out; - } else if (fvp->v_type != VDIR && tvp->v_type == VDIR) { - error = EISDIR; - goto out; + if ((flags & AT_RENAME_EXCHANGE) == 0) { + if (fvp->v_type == VDIR && tvp->v_type != VDIR) { + error = ENOTDIR; + goto out; + } else if (fvp->v_type != VDIR && + tvp->v_type == VDIR) { + error = EISDIR; + goto out; + } } #ifdef CAPABILITIES if (newfd != AT_FDCWD && (tond.ni_resflags & NIRES_ABS) == 0) { diff --git a/sys/sys/fcntl.h b/sys/sys/fcntl.h --- a/sys/sys/fcntl.h +++ b/sys/sys/fcntl.h @@ -260,7 +260,9 @@ #define AT_EMPTY_PATH 0x4000 /* Operate on dirfd if path is empty */ #define AT_RENAME_NOREPLACE 0x0001 /* Fail rename if target exists */ +#define AT_RENAME_EXCHANGE 0x0002 /* */ #define RENAME_NOREPLACE AT_RENAME_NOREPLACE +#define RENAME_EXCHANGE AT_RENAME_EXCHANGE #endif /* __BSD_VISIBLE */ /*