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
This commit is contained in:
Konstantin Belousov 2021-03-07 16:29:09 +02:00
parent d51b4b0aac
commit 509124b626
11 changed files with 185 additions and 34 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 */

View File

@ -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);

View File

@ -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 */
/*

View File

@ -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.