From 509124b62616f73dcdc42263ee109392dafafd99 Mon Sep 17 00:00:00 2001 From: Konstantin Belousov Date: Sun, 7 Mar 2021 16:29:09 +0200 Subject: [PATCH] Add AT_EMPTY_PATH for several *at(2) syscalls It is currently allowed to fchownat(2), fchmodat(2), fchflagsat(2), utimensat(2), fstatat(2), and linkat(2). For linkat(2), PRIV_VFS_FHOPEN privilege is required to exercise the flag. It allows to link any open file. Requested by: trasz Tested by: pho, trasz Reviewed by: markj Sponsored by: The FreeBSD Foundation MFC after: 2 weeks Differential revision: https://reviews.freebsd.org/D29111 --- lib/libc/sys/access.2 | 13 ++++++++- lib/libc/sys/chflags.2 | 13 ++++++++- lib/libc/sys/chmod.2 | 13 ++++++++- lib/libc/sys/chown.2 | 13 ++++++++- lib/libc/sys/link.2 | 11 ++++++- lib/libc/sys/stat.2 | 13 ++++++++- lib/libc/sys/utimensat.2 | 13 ++++++++- sys/kern/vfs_lookup.c | 63 +++++++++++++++++++++++++++++++++++----- sys/kern/vfs_syscalls.c | 63 ++++++++++++++++++++++++++++------------ sys/sys/fcntl.h | 1 + sys/sys/namei.h | 3 ++ 11 files changed, 185 insertions(+), 34 deletions(-) diff --git a/lib/libc/sys/access.2 b/lib/libc/sys/access.2 index 13bfd7e5a88a..12af63385780 100644 --- 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 @@ See the description of the 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 index a44713904599..f8dfd59c39d3 100644 --- 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 @@ See the description of the 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 index 0127a5b629e4..44a1b18718f1 100644 --- 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 @@ See the description of the 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 index 4c45ce9174bb..467ff8a87e55 100644 --- 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 @@ See the description of the 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 index bcf03f17f3bb..37225f9571d0 100644 --- 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 @@ -124,6 +124,15 @@ See the description of the 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 index 0ed70620af63..55221d05a60e 100644 --- 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 @@ See the description of the 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 index d31ee1f1515a..2af452898c9d 100644 --- 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 @@ See the description of the 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 index 07c89e634de4..f4ec3cea9fff 100644 --- a/sys/kern/vfs_lookup.c +++ b/sys/kern/vfs_lookup.c @@ -401,7 +401,9 @@ namei_setup(struct nameidata *ndp, struct vnode **dpp, struct pwd **pwdp) } #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 @@ namei_getpath(struct nameidata *ndp) &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 @@ namei(struct nameidata *ndp) 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 @@ namei(struct nameidata *ndp) 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 index 48df8a3e9051..45f155ebff3d 100644 --- a/sys/kern/vfs_syscalls.c +++ b/sys/kern/vfs_syscalls.c @@ -129,6 +129,8 @@ at2cnpflags(u_int at_flags, u_int mask) 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 @@ sys_linkat(struct thread *td, struct linkat_args *uap) 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; @@ -1578,6 +1581,23 @@ kern_linkat_vp(struct thread *td, struct vnode *vp, int fd, const char *path, LOCKPARENT | SAVENAME | AUDITVNODE2 | NOCACHE, segflag, path, fd, &cap_linkat_target_rights, td); if ((error = namei(&nd)) == 0) { + if ((nd.ni_resflags & NIRES_EMPTYPATH) != 0) { + error = priv_check(td, PRIV_VFS_FHOPEN); + if (error != 0) { + NDFREE(&nd, NDF_ONLY_PNBUF); + if (nd.ni_vp != NULL) { + if (nd.ni_dvp == nd.ni_vp) + vrele(nd.ni_dvp); + else + vput(nd.ni_dvp); + vrele(nd.ni_vp); + } else { + vput(nd.ni_dvp); + } + vrele(vp); + return (error); + } + } if (nd.ni_vp != NULL) { NDFREE(&nd, NDF_ONLY_PNBUF); if (nd.ni_dvp == nd.ni_vp) @@ -2075,7 +2095,7 @@ kern_accessat(struct thread *td, int fd, const char *path, 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 +2116,8 @@ kern_accessat(struct thread *td, int fd, const char *path, 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 +2407,13 @@ kern_statat(struct thread *td, int flag, int fd, const char *path, 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 +2731,8 @@ int 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 +2765,8 @@ kern_chflagsat(struct thread *td, int fd, const char *path, 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 +2860,8 @@ int 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 +2894,8 @@ kern_fchmodat(struct thread *td, int fd, const char *path, 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 +2989,8 @@ int 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 +3006,8 @@ kern_fchownat(struct thread *td, int fd, const char *path, 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 +3358,14 @@ kern_utimensat(struct thread *td, int fd, const char *path, 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 index bc2011c31e88..0fa4e7758c9d 100644 --- a/sys/sys/fcntl.h +++ b/sys/sys/fcntl.h @@ -224,6 +224,7 @@ typedef __pid_t pid_t; /* #define AT_UNUSED1 0x1000 *//* Was AT_BENEATH */ #define AT_RESOLVE_BENEATH 0x2000 /* Do not allow name resolution to walk out of dirfd */ +#define AT_EMPTY_PATH 0x4000 /* Operate on dirfd if path is empty */ #endif /* __BSD_VISIBLE */ /* diff --git a/sys/sys/namei.h b/sys/sys/namei.h index b6985f1fa6ff..5f3d917083a5 100644 --- a/sys/sys/namei.h +++ b/sys/sys/namei.h @@ -144,10 +144,12 @@ int cache_fplookup(struct nameidata *ndp, enum cache_fpl_status *status, #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 @@ int cache_fplookup(struct nameidata *ndp, enum cache_fpl_status *status, */ #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.