diff --git a/lib/libc/sys/access.2 b/lib/libc/sys/access.2 --- a/lib/libc/sys/access.2 +++ b/lib/libc/sys/access.2 @@ -28,7 +28,7 @@ .\" @(#)access.2 8.2 (Berkeley) 4/1/94 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt ACCESS 2 .Os .Sh NAME @@ -129,6 +129,17 @@ flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path +argument is an empty string, operate on the file or directory +referenced by the descriptor +.Fa fd . +If +.Fa fd +is equal to +.Dv AT_FDCWD , +operate on the current working directory. .El .Pp Even if a process's real or effective user has appropriate privileges diff --git a/lib/libc/sys/chflags.2 b/lib/libc/sys/chflags.2 --- a/lib/libc/sys/chflags.2 +++ b/lib/libc/sys/chflags.2 @@ -28,7 +28,7 @@ .\" @(#)chflags.2 8.3 (Berkeley) 5/2/95 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt CHFLAGS 2 .Os .Sh NAME @@ -103,6 +103,17 @@ flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path +argument is an empty string, operate on the file or directory +referenced by the descriptor +.Fa fd . +If +.Fa fd +is equal to +.Dv AT_FDCWD , +operate on the current working directory. .El .Pp If diff --git a/lib/libc/sys/chmod.2 b/lib/libc/sys/chmod.2 --- a/lib/libc/sys/chmod.2 +++ b/lib/libc/sys/chmod.2 @@ -28,7 +28,7 @@ .\" @(#)chmod.2 8.1 (Berkeley) 6/4/93 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt CHMOD 2 .Os .Sh NAME @@ -110,6 +110,17 @@ flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path +argument is an empty string, operate on the file or directory +referenced by the descriptor +.Fa fd . +If +.Fa fd +is equal to +.Dv AT_FDCWD , +operate on the current working directory. .El .Pp If diff --git a/lib/libc/sys/chown.2 b/lib/libc/sys/chown.2 --- a/lib/libc/sys/chown.2 +++ b/lib/libc/sys/chown.2 @@ -28,7 +28,7 @@ .\" @(#)chown.2 8.4 (Berkeley) 4/19/94 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt CHOWN 2 .Os .Sh NAME @@ -127,6 +127,17 @@ flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path +argument is an empty string, operate on the file or directory +referenced by the descriptor +.Fa fd . +If +.Fa fd +is equal to +.Dv AT_FDCWD , +operate on the current working directory. .El .Pp If diff --git a/lib/libc/sys/link.2 b/lib/libc/sys/link.2 --- a/lib/libc/sys/link.2 +++ b/lib/libc/sys/link.2 @@ -28,7 +28,7 @@ .\" @(#)link.2 8.3 (Berkeley) 1/12/94 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt LINK 2 .Os .Sh NAME @@ -117,13 +117,22 @@ created. .It Dv AT_RESOLVE_BENEATH Only walk paths below the directory specified by the -.Ar fd +.Ar fd1 descriptor. See the description of the .Dv O_RESOLVE_BENEATH flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path2 +argument is an empty string, link the file referenced by the descriptor +.Fa fd2 . +The operation requires that the calling process has the +.Dv PRIV_VFS_FHOPEN +privilege, effectively being executed with effective user +.Dv root . .El .Pp If diff --git a/lib/libc/sys/stat.2 b/lib/libc/sys/stat.2 --- a/lib/libc/sys/stat.2 +++ b/lib/libc/sys/stat.2 @@ -28,7 +28,7 @@ .\" @(#)stat.2 8.4 (Berkeley) 5/1/95 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt STAT 2 .Os .Sh NAME @@ -111,6 +111,17 @@ flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path +argument is an empty string, operate on the file or directory +referenced by the descriptor +.Fa fd . +If +.Fa fd +is equal to +.Dv AT_FDCWD , +operate on the current working directory. .El .Pp If diff --git a/lib/libc/sys/utimensat.2 b/lib/libc/sys/utimensat.2 --- a/lib/libc/sys/utimensat.2 +++ b/lib/libc/sys/utimensat.2 @@ -31,7 +31,7 @@ .\" @(#)utimes.2 8.1 (Berkeley) 6/4/93 .\" $FreeBSD$ .\" -.Dd February 23, 2021 +.Dd March 30, 2021 .Dt UTIMENSAT 2 .Os .Sh NAME @@ -155,6 +155,17 @@ flag in the .Xr open 2 manual page. +.It Dv AT_EMPTY_PATH +If the +.Fa path +argument is an empty string, operate on the file or directory +referenced by the descriptor +.Fa fd . +If +.Fa fd +is equal to +.Dv AT_FDCWD , +operate on the current working directory. .El .Sh RETURN VALUES .Rv -std diff --git a/sys/kern/vfs_lookup.c b/sys/kern/vfs_lookup.c --- a/sys/kern/vfs_lookup.c +++ b/sys/kern/vfs_lookup.c @@ -401,7 +401,9 @@ } #endif } - if (error == 0 && (*dpp)->v_type != VDIR) + if (error == 0 && (*dpp)->v_type != VDIR && + (cnp->cn_pnbuf[0] != '\0' || + (cnp->cn_flags & EMPTYPATH) == 0)) error = ENOTDIR; } if (error == 0 && (cnp->cn_flags & RBENEATH) != 0) { @@ -458,23 +460,62 @@ &ndp->ni_pathlen); } - if (__predict_false(error != 0)) { - namei_cleanup_cnp(cnp); + if (__predict_false(error != 0)) return (error); - } /* - * Don't allow empty pathnames. + * Don't allow empty pathnames unless EMPTYPATH is specified. + * Caller checks for ENOENT as an indication for the empty path. */ - if (__predict_false(*cnp->cn_pnbuf == '\0')) { - namei_cleanup_cnp(cnp); + if (__predict_false(*cnp->cn_pnbuf == '\0')) return (ENOENT); - } cnp->cn_nameptr = cnp->cn_pnbuf; return (0); } +static int +namei_emptypath(struct nameidata *ndp) +{ + struct componentname *cnp; + struct pwd *pwd; + struct vnode *dp; + int error; + + cnp = &ndp->ni_cnd; + MPASS(*cnp->cn_pnbuf == '\0'); + MPASS((cnp->cn_flags & EMPTYPATH) != 0); + MPASS((cnp->cn_flags & (LOCKPARENT | WANTPARENT)) == 0); + + error = namei_setup(ndp, &dp, &pwd); + if (error != 0) { + namei_cleanup_cnp(cnp); + goto errout; + } + + ndp->ni_vp = dp; + vref(dp); + namei_cleanup_cnp(cnp); + pwd_drop(pwd); + ndp->ni_resflags |= NIRES_EMPTYPATH; + NDVALIDATE(ndp); + if ((cnp->cn_flags & LOCKLEAF) != 0) { + VOP_LOCK(dp, (cnp->cn_flags & LOCKSHARED) != 0 ? + LK_SHARED : LK_EXCLUSIVE); + if (VN_IS_DOOMED(dp)) { + vput(dp); + error = ENOENT; + goto errout; + } + } + SDT_PROBE4(vfs, namei, lookup, return, 0, ndp->ni_vp, false, ndp); + return (0); + +errout: + SDT_PROBE4(vfs, namei, lookup, return, error, NULL, false, ndp); + return (error); +} + /* * Convert a pathname into a pointer to a locked vnode. * @@ -555,6 +596,11 @@ error = namei_getpath(ndp); if (__predict_false(error != 0)) { + if (error == ENOENT && (cnp->cn_flags & EMPTYPATH) != 0) + return (namei_emptypath(ndp)); + namei_cleanup_cnp(cnp); + SDT_PROBE4(vfs, namei, lookup, return, error, NULL, + false, ndp); return (error); } @@ -588,6 +634,7 @@ ndp->ni_loopcnt = 0; error = namei_getpath(ndp); if (__predict_false(error != 0)) { + namei_cleanup_cnp(cnp); return (error); } /* FALLTHROUGH */ 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 @@ -129,6 +129,8 @@ res |= (at_flags & AT_SYMLINK_NOFOLLOW) != 0 ? NOFOLLOW : FOLLOW; } + if ((mask & AT_EMPTY_PATH) != 0 && (at_flags & AT_EMPTY_PATH) != 0) + res |= EMPTYPATH; return (res); } @@ -1496,12 +1498,13 @@ int flag; flag = uap->flag; - if ((flag & ~(AT_SYMLINK_FOLLOW | AT_RESOLVE_BENEATH)) != 0) + if ((flag & ~(AT_SYMLINK_FOLLOW | AT_RESOLVE_BENEATH | + AT_EMPTY_PATH)) != 0) return (EINVAL); return (kern_linkat(td, uap->fd1, uap->fd2, uap->path1, uap->path2, UIO_USERSPACE, at2cnpflags(flag, AT_SYMLINK_FOLLOW | - AT_RESOLVE_BENEATH))); + AT_RESOLVE_BENEATH | AT_EMPTY_PATH))); } int hardlink_check_uid = 0; @@ -1556,6 +1559,14 @@ path1, fd1, &cap_linkat_source_rights, td); if ((error = namei(&nd)) != 0) return (error); + if ((nd.ni_resflags & NIRES_EMPTYPATH) != 0) { + error = priv_check(td, PRIV_VFS_FHOPEN); + if (error != 0) { + NDFREE(&nd, NDF_ONLY_PNBUF); + vrele(nd.ni_vp); + return (error); + } + } NDFREE(&nd, NDF_ONLY_PNBUF); error = kern_linkat_vp(td, nd.ni_vp, fd2, path2, segflag); } while (error == EAGAIN || error == ERELOOKUP); @@ -2075,7 +2086,7 @@ struct nameidata nd; int error; - if ((flag & ~(AT_EACCESS | AT_RESOLVE_BENEATH)) != 0) + if ((flag & ~(AT_EACCESS | AT_RESOLVE_BENEATH | AT_EMPTY_PATH)) != 0) return (EINVAL); if (amode != F_OK && (amode & ~(R_OK | W_OK | X_OK)) != 0) return (EINVAL); @@ -2096,8 +2107,8 @@ usecred = cred; AUDIT_ARG_VALUE(amode); NDINIT_ATRIGHTS(&nd, LOOKUP, FOLLOW | LOCKSHARED | LOCKLEAF | - AUDITVNODE1 | at2cnpflags(flag, AT_RESOLVE_BENEATH), - pathseg, path, fd, &cap_fstat_rights, td); + AUDITVNODE1 | at2cnpflags(flag, AT_RESOLVE_BENEATH | + AT_EMPTY_PATH), pathseg, path, fd, &cap_fstat_rights, td); if ((error = namei(&nd)) != 0) goto out; vp = nd.ni_vp; @@ -2387,12 +2398,13 @@ struct nameidata nd; int error; - if ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH)) != 0) + if ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH | + AT_EMPTY_PATH)) != 0) return (EINVAL); NDINIT_ATRIGHTS(&nd, LOOKUP, at2cnpflags(flag, AT_RESOLVE_BENEATH | - AT_SYMLINK_NOFOLLOW) | LOCKSHARED | LOCKLEAF | AUDITVNODE1, - pathseg, path, fd, &cap_fstat_rights, td); + AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH) | LOCKSHARED | LOCKLEAF | + AUDITVNODE1, pathseg, path, fd, &cap_fstat_rights, td); if ((error = namei(&nd)) != 0) return (error); @@ -2710,7 +2722,8 @@ sys_chflagsat(struct thread *td, struct chflagsat_args *uap) { - if ((uap->atflag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH)) != 0) + if ((uap->atflag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH | + AT_EMPTY_PATH)) != 0) return (EINVAL); return (kern_chflagsat(td, uap->fd, uap->path, UIO_USERSPACE, @@ -2743,8 +2756,8 @@ AUDIT_ARG_FFLAGS(flags); NDINIT_ATRIGHTS(&nd, LOOKUP, at2cnpflags(atflag, AT_SYMLINK_NOFOLLOW | - AT_RESOLVE_BENEATH) | AUDITVNODE1, pathseg, path, fd, - &cap_fchflags_rights, td); + AT_RESOLVE_BENEATH | AT_EMPTY_PATH) | AUDITVNODE1, pathseg, path, + fd, &cap_fchflags_rights, td); if ((error = namei(&nd)) != 0) return (error); NDFREE_NOTHING(&nd); @@ -2838,7 +2851,8 @@ sys_fchmodat(struct thread *td, struct fchmodat_args *uap) { - if ((uap->flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH)) != 0) + if ((uap->flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH | + AT_EMPTY_PATH)) != 0) return (EINVAL); return (kern_fchmodat(td, uap->fd, uap->path, UIO_USERSPACE, @@ -2871,8 +2885,8 @@ AUDIT_ARG_MODE(mode); NDINIT_ATRIGHTS(&nd, LOOKUP, at2cnpflags(flag, AT_SYMLINK_NOFOLLOW | - AT_RESOLVE_BENEATH) | AUDITVNODE1, pathseg, path, fd, - &cap_fchmod_rights, td); + AT_RESOLVE_BENEATH | AT_EMPTY_PATH) | AUDITVNODE1, pathseg, path, + fd, &cap_fchmod_rights, td); if ((error = namei(&nd)) != 0) return (error); NDFREE_NOTHING(&nd); @@ -2966,7 +2980,8 @@ sys_fchownat(struct thread *td, struct fchownat_args *uap) { - if ((uap->flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH)) != 0) + if ((uap->flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH | + AT_EMPTY_PATH)) != 0) return (EINVAL); return (kern_fchownat(td, uap->fd, uap->path, UIO_USERSPACE, uap->uid, @@ -2982,8 +2997,8 @@ AUDIT_ARG_OWNER(uid, gid); NDINIT_ATRIGHTS(&nd, LOOKUP, at2cnpflags(flag, AT_SYMLINK_NOFOLLOW | - AT_RESOLVE_BENEATH) | AUDITVNODE1, pathseg, path, fd, - &cap_fchown_rights, td); + AT_RESOLVE_BENEATH | AT_EMPTY_PATH) | AUDITVNODE1, pathseg, path, + fd, &cap_fchown_rights, td); if ((error = namei(&nd)) != 0) return (error); @@ -3334,13 +3349,14 @@ struct timespec ts[2]; int error, flags; - if ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH)) != 0) + if ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_RESOLVE_BENEATH | + AT_EMPTY_PATH)) != 0) return (EINVAL); if ((error = getutimens(tptr, tptrseg, ts, &flags)) != 0) return (error); NDINIT_ATRIGHTS(&nd, LOOKUP, at2cnpflags(flag, AT_SYMLINK_NOFOLLOW | - AT_RESOLVE_BENEATH) | AUDITVNODE1, + AT_RESOLVE_BENEATH | AT_EMPTY_PATH) | AUDITVNODE1, pathseg, path, fd, &cap_futimes_rights, td); if ((error = namei(&nd)) != 0) return (error); diff --git a/sys/sys/fcntl.h b/sys/sys/fcntl.h --- a/sys/sys/fcntl.h +++ b/sys/sys/fcntl.h @@ -219,10 +219,13 @@ #define AT_SYMLINK_NOFOLLOW 0x0200 /* Do not follow symbolic links */ #define AT_SYMLINK_FOLLOW 0x0400 /* Follow symbolic link */ #define AT_REMOVEDIR 0x0800 /* Remove directory instead of file */ +#endif /* __POSIX_VISIBLE >= 200809 */ +#if __BSD_VISIBLE /* #define AT_UNUSED1 0x1000 *//* Was AT_BENEATH */ #define AT_RESOLVE_BENEATH 0x2000 /* Do not allow name resolution to walk out of dirfd */ -#endif +#define AT_EMPTY_PATH 0x4000 /* Operate on dirfd if path is empty */ +#endif /* __BSD_VISIBLE */ /* * Constants used for fcntl(2) diff --git a/sys/sys/namei.h b/sys/sys/namei.h --- a/sys/sys/namei.h +++ b/sys/sys/namei.h @@ -144,10 +144,12 @@ #define WANTPARENT 0x0010 /* want parent vnode returned unlocked */ #define FAILIFEXISTS 0x0020 /* return EEXIST if found */ #define FOLLOW 0x0040 /* follow symbolic links */ +#define EMPTYPATH 0x0080 /* Allow empty path for *at */ #define LOCKSHARED 0x0100 /* Shared lock leaf */ #define NOFOLLOW 0x0000 /* do not follow symbolic links (pseudo) */ #define RBENEATH 0x100000000ULL /* No escape, even tmp, from start dir */ #define MODMASK 0xf000001ffULL /* mask of operational modifiers */ + /* * Namei parameter descriptors. * @@ -198,6 +200,7 @@ */ #define NIRES_ABS 0x00000001 /* Path was absolute */ #define NIRES_STRICTREL 0x00000002 /* Restricted lookup result */ +#define NIRES_EMPTYPATH 0x00000004 /* EMPTYPATH used */ /* * Flags in ni_lcf, valid for the duration of the namei call.