diff options
Diffstat (limited to 'tests/sys/file/path_test.c')
| -rw-r--r-- | tests/sys/file/path_test.c | 1039 | 
1 files changed, 1039 insertions, 0 deletions
| diff --git a/tests/sys/file/path_test.c b/tests/sys/file/path_test.c new file mode 100644 index 000000000000..b3b8b7cebd4d --- /dev/null +++ b/tests/sys/file/path_test.c @@ -0,0 +1,1039 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2021 The FreeBSD Foundation + * + * This software was developed by Mark Johnston under sponsorship from + * the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * 1. Redistributions of source code must retain the above copyright + *    notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + *    notice, this list of conditions and the following disclaimer in + *    the documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Basic regression tests for handling of O_PATH descriptors. + */ + +#include <sys/param.h> +#include <sys/capsicum.h> +#include <sys/event.h> +#include <sys/ioctl.h> +#include <sys/memrange.h> +#include <sys/mman.h> +#include <sys/ptrace.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/uio.h> +#include <sys/un.h> +#include <sys/wait.h> + +#include <aio.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include <atf-c.h> + +#define	FMT_ERR(s)		s ": %s", strerror(errno) + +#define	CHECKED_CLOSE(fd)	\ +	ATF_REQUIRE_MSG(close(fd) == 0, FMT_ERR("close")) + +/* Create a temporary regular file containing some data. */ +static void +mktfile(char path[PATH_MAX], const char *template) +{ +	char buf[BUFSIZ]; +	int fd; + +	snprintf(path, PATH_MAX, "%s", template); +	fd = mkstemp(path); +	ATF_REQUIRE_MSG(fd >= 0, FMT_ERR("mkstemp")); +	memset(buf, 0, sizeof(buf)); +	ATF_REQUIRE_MSG(write(fd, buf, sizeof(buf)) == sizeof(buf), +	    FMT_ERR("write")); +	CHECKED_CLOSE(fd); +} + +/* Make a temporary directory. */ +static void +mktdir(char path[PATH_MAX], const char *template) +{ +	snprintf(path, PATH_MAX, "%s", template); +	ATF_REQUIRE_MSG(mkdtemp(path) == path, FMT_ERR("mkdtemp")); +} + +/* Wait for a child process to exit with status 0. */ +static void +waitchild(pid_t child, int exstatus) +{ +	int error, status; + +	error = waitpid(child, &status, 0); +	ATF_REQUIRE_MSG(error != -1, FMT_ERR("waitpid")); +	ATF_REQUIRE_MSG(WIFEXITED(status), "child exited abnormally, status %d", +	    status); +	ATF_REQUIRE_MSG(WEXITSTATUS(status) == exstatus, +	    "child exit status is %d, expected %d", +	    WEXITSTATUS(status), exstatus); +} + +ATF_TC_WITHOUT_HEAD(path_access); +ATF_TC_BODY(path_access, tc) +{ +	char path[PATH_MAX]; +	struct stat sb; +	struct timespec ts[2]; +	struct timeval tv[2]; +	int pathfd; + +	mktfile(path, "path_access.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE_ERRNO(EBADF, fchmod(pathfd, 0666) == -1); +	ATF_REQUIRE_ERRNO(EBADF, fchown(pathfd, getuid(), getgid()) == -1); +	ATF_REQUIRE_ERRNO(EBADF, fchflags(pathfd, UF_NODUMP) == -1); +	memset(tv, 0, sizeof(tv)); +	ATF_REQUIRE_ERRNO(EBADF, futimes(pathfd, tv) == -1); +	memset(ts, 0, sizeof(ts)); +	ATF_REQUIRE_ERRNO(EBADF, futimens(pathfd, ts) == -1); + +	/* fpathconf(2) and fstat(2) are permitted. */ +	ATF_REQUIRE_MSG(fstat(pathfd, &sb) == 0, FMT_ERR("fstat")); +	ATF_REQUIRE_MSG(fpathconf(pathfd, _PC_LINK_MAX) != -1, +	    FMT_ERR("fpathconf")); + +	CHECKED_CLOSE(pathfd); +} + +/* Basic tests to verify that AIO operations fail. */ +ATF_TC_WITHOUT_HEAD(path_aio); +ATF_TC_BODY(path_aio, tc) +{ +	struct aiocb aio; +	char buf[BUFSIZ], path[PATH_MAX]; +	int pathfd; + +	mktfile(path, "path_aio.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	memset(&aio, 0, sizeof(aio)); +	aio.aio_buf = buf; +	aio.aio_nbytes = sizeof(buf); +	aio.aio_fildes = pathfd; +	aio.aio_offset = 0; + +	ATF_REQUIRE_ERRNO(EBADF, aio_read(&aio) == -1); +	ATF_REQUIRE_ERRNO(EBADF, aio_write(&aio) == -1); +	ATF_REQUIRE_ERRNO(EBADF, aio_fsync(O_SYNC, &aio) == -1); +	ATF_REQUIRE_ERRNO(EBADF, aio_fsync(O_DSYNC, &aio) == -1); + +	CHECKED_CLOSE(pathfd); +} + +/* Basic tests to verify that Capsicum restrictions apply to path fds. */ +ATF_TC_WITHOUT_HEAD(path_capsicum); +ATF_TC_BODY(path_capsicum, tc) +{ +	char path[PATH_MAX]; +	cap_rights_t rights; +	int truefd; +	pid_t child; + +	mktfile(path, "path_capsicum.XXXXXX"); + +	/* Make sure that filesystem namespace restrictions apply to O_PATH. */ +	child = fork(); +	ATF_REQUIRE_MSG(child != -1, FMT_ERR("fork")); +	if (child == 0) { +		if (cap_enter() != 0) +			_exit(1); +		if (open(path, O_PATH) >= 0) +			_exit(2); +		if (errno != ECAPMODE) +			_exit(3); +		if (open("/usr/bin/true", O_PATH | O_EXEC) >= 0) +			_exit(4); +		if (errno != ECAPMODE) +			_exit(5); +		_exit(0); +	} +	waitchild(child, 0); + +	/* Make sure that CAP_FEXECVE is required. */ +	child = fork(); +	ATF_REQUIRE_MSG(child != -1, FMT_ERR("fork")); +	if (child == 0) { +		truefd = open("/usr/bin/true", O_PATH | O_EXEC); +		if (truefd < 0) +			_exit(1); +		cap_rights_init(&rights); +		if (cap_rights_limit(truefd, &rights) != 0) +			_exit(2); +		(void)fexecve(truefd, +		    (char * const[]){__DECONST(char *, "/usr/bin/true"), NULL}, +		    NULL); +		if (errno != ENOTCAPABLE) +			_exit(3); +		_exit(4); +	} +	waitchild(child, 4); +} + +/* + * Check that a pathfd can be converted to a regular fd using openat() in + * capability mode, but that rights on the pathfd are respected. + */ +ATF_TC_WITHOUT_HEAD(path_capsicum_empty); +ATF_TC_BODY(path_capsicum_empty, tc) +{ +	char path[PATH_MAX]; +	cap_rights_t rights; +	int dfd, fd, pathfd, pathdfd; + +	mktfile(path, "path_capsicum.XXXXXX"); + +	pathdfd = open(".", O_PATH); +	ATF_REQUIRE_MSG(pathdfd >= 0, FMT_ERR("open")); +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE(cap_enter() == 0); + +	dfd = openat(pathdfd, "", O_DIRECTORY | O_EMPTY_PATH); +	ATF_REQUIRE(dfd >= 0); +	CHECKED_CLOSE(dfd); + +	/* +	 * CAP_READ and CAP_LOOKUP should be sufficient to open a directory. +	 */ +	cap_rights_init(&rights, CAP_READ, CAP_LOOKUP); +	ATF_REQUIRE(cap_rights_limit(pathdfd, &rights) == 0); +	dfd = openat(pathdfd, "", O_DIRECTORY | O_EMPTY_PATH); +	ATF_REQUIRE(dfd >= 0); +	CHECKED_CLOSE(dfd); + +	/* +	 * ... CAP_READ on its own is not. +	 */ +	cap_rights_init(&rights, CAP_READ); +	ATF_REQUIRE(cap_rights_limit(pathdfd, &rights) == 0); +	dfd = openat(pathdfd, "", O_DIRECTORY | O_EMPTY_PATH); +	ATF_REQUIRE_ERRNO(ENOTCAPABLE, dfd == -1); + +	/* +	 * Now try with a regular file. +	 */ +	fd = openat(pathfd, "", O_RDWR | O_EMPTY_PATH); +	ATF_REQUIRE(fd >= 0); +	CHECKED_CLOSE(fd); + +	cap_rights_init(&rights, CAP_READ, CAP_LOOKUP, CAP_WRITE); +	ATF_REQUIRE(cap_rights_limit(pathfd, &rights) == 0); +	fd = openat(pathfd, "", O_RDWR | O_EMPTY_PATH | O_APPEND); +	ATF_REQUIRE(fd >= 0); +	CHECKED_CLOSE(fd); + +	/* +	 * CAP_SEEK is needed to open a file for writing without O_APPEND. +	 */ +	cap_rights_init(&rights, CAP_READ, CAP_LOOKUP, CAP_WRITE); +	ATF_REQUIRE(cap_rights_limit(pathfd, &rights) == 0); +	fd = openat(pathfd, "", O_RDWR | O_EMPTY_PATH); +	ATF_REQUIRE_ERRNO(ENOTCAPABLE, fd == -1); + +	/* +	 * CAP_LOOKUP isn't sufficient to open a file for reading. +	 */ +	cap_rights_init(&rights, CAP_LOOKUP); +	ATF_REQUIRE(cap_rights_limit(pathfd, &rights) == 0); +	fd = openat(pathfd, "", O_RDONLY | O_EMPTY_PATH); +	ATF_REQUIRE_ERRNO(ENOTCAPABLE, fd == -1); + +	CHECKED_CLOSE(pathfd); +	CHECKED_CLOSE(pathdfd); +} + +/* Make sure that ptrace(PT_COREDUMP) cannot be used to write to a path fd. */ +ATF_TC_WITHOUT_HEAD(path_coredump); +ATF_TC_BODY(path_coredump, tc) +{ +	char path[PATH_MAX]; +	struct ptrace_coredump pc; +	int error, pathfd, status; +	pid_t child; + +	mktdir(path, "path_coredump.XXXXXX"); + +	child = fork(); +	ATF_REQUIRE_MSG(child != -1, FMT_ERR("fork")); +	if (child == 0) { +		while (true) +			(void)sleep(1); +	} + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	error = ptrace(PT_ATTACH, child, 0, 0); +	ATF_REQUIRE_MSG(error == 0, FMT_ERR("ptrace")); +	error = waitpid(child, &status, 0); +	ATF_REQUIRE_MSG(error != -1, FMT_ERR("waitpid")); +	ATF_REQUIRE_MSG(WIFSTOPPED(status), "unexpected status %d", status); + +	pc.pc_fd = pathfd; +	pc.pc_flags = 0; +	pc.pc_limit = 0; +	error = ptrace(PT_COREDUMP, child, (void *)&pc, sizeof(pc)); +	ATF_REQUIRE_ERRNO(EBADF, error == -1); + +	error = ptrace(PT_DETACH, child, 0, 0); +	ATF_REQUIRE_MSG(error == 0, FMT_ERR("ptrace")); + +	ATF_REQUIRE_MSG(kill(child, SIGKILL) == 0, FMT_ERR("kill")); + +	CHECKED_CLOSE(pathfd); +} + +/* Verify operations on directory path descriptors. */ +ATF_TC_WITHOUT_HEAD(path_directory); +ATF_TC_BODY(path_directory, tc) +{ +	struct dirent de; +	struct stat sb; +	char path[PATH_MAX]; +	int fd, pathfd; + +	mktdir(path, "path_directory.XXXXXX"); + +	pathfd = open(path, O_PATH | O_DIRECTORY); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	/* Should not be possible to list directory entries. */ +	ATF_REQUIRE_ERRNO(EBADF, +	    getdirentries(pathfd, (char *)&de, sizeof(de), NULL) == -1); + +	/* It should be possible to create files under pathfd. */ +	fd = openat(pathfd, "test", O_RDWR | O_CREAT, 0600); +	ATF_REQUIRE_MSG(fd >= 0, FMT_ERR("open")); +	ATF_REQUIRE_MSG(fstatat(pathfd, "test", &sb, 0) == 0, +	    FMT_ERR("fstatat")); +	CHECKED_CLOSE(fd); + +	/* ... but doing so requires write access. */ +	if (geteuid() != 0) { +		ATF_REQUIRE_ERRNO(EBADF, fchmod(pathfd, 0500) == -1); +		ATF_REQUIRE_MSG(chmod(path, 0500) == 0, FMT_ERR("chmod")); +		ATF_REQUIRE_ERRNO(EACCES, +		    openat(pathfd, "test2", O_RDWR | O_CREAT, 0600) < 0); +	} + +	/* fchdir(2) is permitted. */ +	ATF_REQUIRE_MSG(fchdir(pathfd) == 0, FMT_ERR("fchdir")); + +	CHECKED_CLOSE(pathfd); +} + +/* Verify access permission checking for a directory path fd. */ +ATF_TC_WITH_CLEANUP(path_directory_not_root); +ATF_TC_HEAD(path_directory_not_root, tc) +{ +	atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(path_directory_not_root, tc) +{ +	char path[PATH_MAX]; +	int pathfd; + +	mktdir(path, "path_directory.XXXXXX"); + +	pathfd = open(path, O_PATH | O_DIRECTORY); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE_ERRNO(EBADF, fchmod(pathfd, 0500) == -1); +	ATF_REQUIRE_MSG(chmod(path, 0500) == 0, FMT_ERR("chmod")); +	ATF_REQUIRE_ERRNO(EACCES, +	    openat(pathfd, "test2", O_RDWR | O_CREAT, 0600) < 0); + +	CHECKED_CLOSE(pathfd); +} +ATF_TC_CLEANUP(path_directory_not_root, tc) +{ +} + +/* Validate system calls that handle AT_EMPTY_PATH. */ +ATF_TC_WITHOUT_HEAD(path_empty); +ATF_TC_BODY(path_empty, tc) +{ +	char path[PATH_MAX]; +	struct timespec ts[2]; +	struct stat sb; +	int pathfd; + +	mktfile(path, "path_empty.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	/* Various *at operations should work on path fds. */ +	ATF_REQUIRE_MSG(faccessat(pathfd, "", F_OK, AT_EMPTY_PATH) == 0, +	    FMT_ERR("faccessat")); +	ATF_REQUIRE_MSG(chflagsat(pathfd, "", UF_NODUMP, AT_EMPTY_PATH) == 0, +	    FMT_ERR("chflagsat")); +	ATF_REQUIRE_MSG(fchmodat(pathfd, "", 0600, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fchmodat")); +	ATF_REQUIRE_MSG(fchownat(pathfd, "", getuid(), getgid(), +	    AT_EMPTY_PATH) == 0, FMT_ERR("fchownat")); +	ATF_REQUIRE_MSG(fstatat(pathfd, "", &sb, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fstatat")); +	ATF_REQUIRE_MSG(sb.st_size == BUFSIZ, +	    "unexpected size %ju", (uintmax_t)sb.st_size); +	memset(ts, 0, sizeof(ts)); +	ATF_REQUIRE_MSG(utimensat(pathfd, "", ts, AT_EMPTY_PATH) == 0, +	    FMT_ERR("utimensat")); + +	CHECKED_CLOSE(pathfd); +} + +/* Verify that various operations on a path fd have access checks. */ +ATF_TC_WITH_CLEANUP(path_empty_not_root); +ATF_TC_HEAD(path_empty_not_root, tc) +{ +	atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(path_empty_not_root, tc) +{ +	int pathfd; + +	pathfd = open("/dev/null", O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE_ERRNO(EPERM, +	    chflagsat(pathfd, "", UF_NODUMP, AT_EMPTY_PATH) == -1); +	ATF_REQUIRE_ERRNO(EPERM, +	    fchownat(pathfd, "", getuid(), getgid(), AT_EMPTY_PATH) == -1); +	ATF_REQUIRE_ERRNO(EPERM, +	    fchmodat(pathfd, "", 0600, AT_EMPTY_PATH) == -1); +	ATF_REQUIRE_ERRNO(EPERM, +	    linkat(pathfd, "", AT_FDCWD, "test", AT_EMPTY_PATH) == -1); + +	CHECKED_CLOSE(pathfd); +} +ATF_TC_CLEANUP(path_empty_not_root, tc) +{ +} + +/* Test linkat(2) with AT_EMPTY_PATH, which requires privileges. */ +ATF_TC_WITH_CLEANUP(path_empty_root); +ATF_TC_HEAD(path_empty_root, tc) +{ +	atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(path_empty_root, tc) +{ +	char path[PATH_MAX]; +	struct stat sb, sb2; +	int pathfd; + +	mktfile(path, "path_empty_root.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); +	ATF_REQUIRE_MSG(fstatat(pathfd, "", &sb, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fstatat")); + +	ATF_REQUIRE_MSG(linkat(pathfd, "", AT_FDCWD, "test", AT_EMPTY_PATH) == +	    0, FMT_ERR("linkat")); +	ATF_REQUIRE_MSG(fstatat(AT_FDCWD, "test", &sb2, 0) == 0, +	    FMT_ERR("fstatat")); +	ATF_REQUIRE_MSG(sb.st_dev == sb2.st_dev, "st_dev mismatch"); +	ATF_REQUIRE_MSG(sb.st_ino == sb2.st_ino, "st_ino mismatch"); + +	CHECKED_CLOSE(pathfd); + +} +ATF_TC_CLEANUP(path_empty_root, tc) +{ +} + +/* poll(2) never returns an event for path fds, but kevent(2) does. */ +ATF_TC_WITHOUT_HEAD(path_event); +ATF_TC_BODY(path_event, tc) +{ +	char buf[BUFSIZ], path[PATH_MAX]; +	struct kevent ev; +	struct pollfd pollfd; +	int kq, pathfd; + +	mktfile(path, "path_event.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	/* poll(2) should return POLLNVAL. */ +	pollfd.fd = pathfd; +	pollfd.events = POLLIN; +	pollfd.revents = 0; +	ATF_REQUIRE_MSG(poll(&pollfd, 1, 0) == 1, FMT_ERR("poll")); +	ATF_REQUIRE_MSG(pollfd.revents == POLLNVAL, "unexpected revents %x", +	    pollfd.revents); +	pollfd.events = POLLOUT; +	pollfd.revents = 0; +	ATF_REQUIRE_MSG(poll(&pollfd, 1, 0) == 1, FMT_ERR("poll")); +	ATF_REQUIRE_MSG(pollfd.revents == POLLNVAL, "unexpected revents %x", +	    pollfd.revents); + +	/* Try to get a EVFILT_READ event through a path fd. */ +	kq = kqueue(); +	ATF_REQUIRE_MSG(kq >= 0, FMT_ERR("kqueue")); +	EV_SET(&ev, pathfd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0); +	ATF_REQUIRE_MSG(kevent(kq, &ev, 1, NULL, 0, NULL) == 0, +	    FMT_ERR("kevent")); +	ATF_REQUIRE_MSG(kevent(kq, NULL, 0, &ev, 1, NULL) == 1, +	    FMT_ERR("kevent")); +	ATF_REQUIRE_MSG((ev.flags & EV_ERROR) == 0, "EV_ERROR is set"); +	ATF_REQUIRE_MSG(ev.data == sizeof(buf), +	    "data is %jd", (intmax_t)ev.data); +	EV_SET(&ev, pathfd, EVFILT_READ, EV_DELETE, 0, 0, 0); +	ATF_REQUIRE_MSG(kevent(kq, &ev, 1, NULL, 0, NULL) == 0, +	    FMT_ERR("kevent")); + +	/* Try to get a EVFILT_VNODE/NOTE_DELETE event through a path fd. */ +	EV_SET(&ev, pathfd, EVFILT_VNODE, EV_ADD | EV_ENABLE, NOTE_DELETE, 0, +	    0); +	ATF_REQUIRE_MSG(kevent(kq, &ev, 1, NULL, 0, NULL) == 0, +	    FMT_ERR("kevent")); +	ATF_REQUIRE_MSG(funlinkat(AT_FDCWD, path, pathfd, 0) == 0, +	    FMT_ERR("funlinkat")); +	ATF_REQUIRE_MSG(kevent(kq, NULL, 0, &ev, 1, NULL) == 1, +	    FMT_ERR("kevent")); +	ATF_REQUIRE_MSG(ev.fflags == NOTE_DELETE, +	    "unexpected fflags %#x", ev.fflags); +	EV_SET(&ev, pathfd, EVFILT_VNODE, EV_DELETE, 0, 0, 0); +	ATF_REQUIRE_MSG(kevent(kq, &ev, 1, NULL, 0, NULL) == 0, +	    FMT_ERR("kevent")); + +	CHECKED_CLOSE(kq); +	CHECKED_CLOSE(pathfd); +} + +/* Check various fcntl(2) operations on a path desriptor. */ +ATF_TC_WITHOUT_HEAD(path_fcntl); +ATF_TC_BODY(path_fcntl, tc) +{ +	char path[PATH_MAX]; +	int flags, pathfd, pathfd2; + +	mktfile(path, "path_fcntl.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	/* O_PATH should appear in the fd flags. */ +	flags = fcntl(pathfd, F_GETFL); +	ATF_REQUIRE_MSG(flags != -1, FMT_ERR("fcntl")); +	ATF_REQUIRE_MSG((flags & O_PATH) != 0, "O_PATH not set"); + +	ATF_REQUIRE_ERRNO(EBADF, +	    fcntl(pathfd, F_SETFL, flags & ~O_PATH)); +	ATF_REQUIRE_ERRNO(EBADF, +	    fcntl(pathfd, F_SETFL, flags | O_APPEND)); + +	/* A dup'ed O_PATH fd had better have O_PATH set too. */ +	pathfd2 = fcntl(pathfd, F_DUPFD, 0); +	ATF_REQUIRE_MSG(pathfd2 >= 0, FMT_ERR("fcntl")); +	flags = fcntl(pathfd2, F_GETFL); +	ATF_REQUIRE_MSG(flags != -1, FMT_ERR("fcntl")); +	ATF_REQUIRE_MSG((flags & O_PATH) != 0, "O_PATH not set"); +	CHECKED_CLOSE(pathfd2); + +	/* Double check with dup(2). */ +	pathfd2 = dup(pathfd); +	ATF_REQUIRE_MSG(pathfd2 >= 0, FMT_ERR("dup")); +	flags = fcntl(pathfd2, F_GETFL); +	ATF_REQUIRE_MSG(flags != -1, FMT_ERR("fcntl")); +	ATF_REQUIRE_MSG((flags & O_PATH) != 0, "O_PATH not set"); +	CHECKED_CLOSE(pathfd2); + +	/* It should be possible to set O_CLOEXEC. */ +	ATF_REQUIRE_MSG(fcntl(pathfd, F_SETFD, FD_CLOEXEC) == 0, +	    FMT_ERR("fcntl")); +	ATF_REQUIRE_MSG(fcntl(pathfd, F_GETFD) == FD_CLOEXEC, +	    FMT_ERR("fcntl")); + +	CHECKED_CLOSE(pathfd); +} + +/* Verify that we can execute a file opened with O_PATH. */ +ATF_TC_WITHOUT_HEAD(path_fexecve); +ATF_TC_BODY(path_fexecve, tc) +{ +	char path[PATH_MAX]; +	pid_t child; +	int fd, pathfd; + +	child = fork(); +	ATF_REQUIRE_MSG(child != -1, FMT_ERR("fork")); +	if (child == 0) { +		pathfd = open("/usr/bin/true", O_PATH | O_EXEC); +		if (pathfd < 0) +			_exit(1); +		fexecve(pathfd, +		    (char * const[]){__DECONST(char *, "/usr/bin/true"), NULL}, +		    NULL); +		_exit(2); +	} +	waitchild(child, 0); + +	/* +	 * Also verify that access permissions are checked when opening with +	 * O_PATH. +	 */ +	snprintf(path, sizeof(path), "path_fexecve.XXXXXX"); +	ATF_REQUIRE_MSG(mktemp(path) == path, FMT_ERR("mktemp")); + +	fd = open(path, O_CREAT | O_RDONLY, 0600); +	ATF_REQUIRE_MSG(fd >= 0, FMT_ERR("open")); + +	pathfd = open(path, O_PATH | O_EXEC); +	ATF_REQUIRE_ERRNO(EACCES, pathfd < 0); +} + +/* Make sure that O_PATH restrictions apply to named pipes as well. */ +ATF_TC_WITHOUT_HEAD(path_fifo); +ATF_TC_BODY(path_fifo, tc) +{ +	char path[PATH_MAX], buf[BUFSIZ]; +	struct kevent ev; +	int kq, pathfd; + +	snprintf(path, sizeof(path), "path_fifo.XXXXXX"); +	ATF_REQUIRE_MSG(mktemp(path) == path, FMT_ERR("mktemp")); + +	ATF_REQUIRE_MSG(mkfifo(path, 0666) == 0, FMT_ERR("mkfifo")); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); +	memset(buf, 0, sizeof(buf)); +	ATF_REQUIRE_ERRNO(EBADF, write(pathfd, buf, sizeof(buf))); +	ATF_REQUIRE_ERRNO(EBADF, read(pathfd, buf, sizeof(buf))); + +	kq = kqueue(); +	ATF_REQUIRE_MSG(kq >= 0, FMT_ERR("kqueue")); +	EV_SET(&ev, pathfd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0); +	ATF_REQUIRE_ERRNO(EBADF, kevent(kq, &ev, 1, NULL, 0, NULL) == -1); + +	CHECKED_CLOSE(pathfd); +} + +/* Files may be unlinked using a path fd. */ +ATF_TC_WITHOUT_HEAD(path_funlinkat); +ATF_TC_BODY(path_funlinkat, tc) +{ +	char path[PATH_MAX]; +	struct stat sb; +	int pathfd; + +	mktfile(path, "path_rights.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE_MSG(funlinkat(AT_FDCWD, path, pathfd, 0) == 0, +	    FMT_ERR("funlinkat")); +	ATF_REQUIRE_ERRNO(ENOENT, stat(path, &sb) == -1); + +	CHECKED_CLOSE(pathfd); +} + +/* Verify that various I/O operations fail on an O_PATH descriptor. */ +ATF_TC_WITHOUT_HEAD(path_io); +ATF_TC_BODY(path_io, tc) +{ +	char path[PATH_MAX], path2[PATH_MAX]; +	char buf[BUFSIZ]; +	struct iovec iov; +	size_t page_size; +	int error, fd, pathfd, sd[2]; + +	/* It is allowed to create new files with O_PATH. */ +	snprintf(path, sizeof(path), "path_io.XXXXXX"); +	ATF_REQUIRE_MSG(mktemp(path) == path, FMT_ERR("mktemp")); +	pathfd = open(path, O_PATH | O_CREAT, 0600); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open(O_PATH|O_CREAT)")); +	/* Ensure that this is indeed O_PATH fd */ +	ATF_REQUIRE_ERRNO(EBADF, write(pathfd, path, strlen(path)) == -1); +	CHECKED_CLOSE(pathfd); + +	/* Create a non-empty file for use in the rest of the tests. */ +	mktfile(path, "path_io.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	/* Make sure that basic I/O operations aren't possible. */ +	iov.iov_base = path; +	iov.iov_len = strlen(path); +	ATF_REQUIRE_ERRNO(EBADF, +	    write(pathfd, iov.iov_base, iov.iov_len) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    pwrite(pathfd, iov.iov_base, iov.iov_len, 0) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    writev(pathfd, &iov, 1) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    pwritev(pathfd, &iov, 1, 0) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    read(pathfd, path, 1) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    pread(pathfd, path, 1, 0) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    readv(pathfd, &iov, 1) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    preadv(pathfd, &iov, 1, 0) == -1); + +	/* copy_file_range() should not be permitted. */ +	mktfile(path2, "path_io.XXXXXX"); +	fd = open(path2, O_RDWR); +	ATF_REQUIRE_ERRNO(EBADF, +	    copy_file_range(fd, NULL, pathfd, NULL, sizeof(buf), 0) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    copy_file_range(pathfd, NULL, fd, NULL, sizeof(buf), 0) == -1); +	CHECKED_CLOSE(fd); + +	/* sendfile() should not be permitted. */ +	ATF_REQUIRE_MSG(socketpair(PF_LOCAL, SOCK_STREAM, 0, sd) == 0, +	    FMT_ERR("socketpair")); +	ATF_REQUIRE_ERRNO(EBADF, +	    sendfile(pathfd, sd[0], 0, 0, NULL, NULL, 0)); +	CHECKED_CLOSE(sd[0]); +	CHECKED_CLOSE(sd[1]); + +	/* No seeking. */ +	ATF_REQUIRE_ERRNO(ESPIPE, +	    lseek(pathfd, 0, SEEK_SET) == -1); + +	/* No operations on the file extent. */ +	ATF_REQUIRE_ERRNO(EINVAL, +	    ftruncate(pathfd, 0) == -1); +	error = posix_fallocate(pathfd, 0, sizeof(buf) * 2); +	ATF_REQUIRE_MSG(error == ESPIPE, "posix_fallocate() returned %d", error); +	error = posix_fadvise(pathfd, 0, sizeof(buf), POSIX_FADV_NORMAL); +	ATF_REQUIRE_MSG(error == ESPIPE, "posix_fadvise() returned %d", error); + +	/* mmap() is not allowed. */ +	page_size = getpagesize(); +	ATF_REQUIRE_ERRNO(ENODEV, +	    mmap(NULL, page_size, PROT_READ, MAP_SHARED, pathfd, 0) == +	    MAP_FAILED); +	ATF_REQUIRE_ERRNO(ENODEV, +	    mmap(NULL, page_size, PROT_NONE, MAP_SHARED, pathfd, 0) == +	    MAP_FAILED); +	ATF_REQUIRE_ERRNO(ENODEV, +	    mmap(NULL, page_size, PROT_READ, MAP_PRIVATE, pathfd, 0) == +	    MAP_FAILED); + +	/* No fsync() or fdatasync(). */ +	ATF_REQUIRE_ERRNO(EBADF, fsync(pathfd) == -1); +	ATF_REQUIRE_ERRNO(EBADF, fdatasync(pathfd) == -1); + +	CHECKED_CLOSE(pathfd); +} + +/* ioctl(2) is not permitted on path fds. */ +ATF_TC_WITHOUT_HEAD(path_ioctl); +ATF_TC_BODY(path_ioctl, tc) +{ +	char path[PATH_MAX]; +	struct mem_extract me; +	int pathfd, val; + +	mktfile(path, "path_ioctl.XXXXXX"); + +	/* Standard file descriptor ioctls should fail. */ +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	val = 0; +	ATF_REQUIRE_ERRNO(EBADF, ioctl(pathfd, FIONBIO, &val) == -1); +	ATF_REQUIRE_ERRNO(EBADF, ioctl(pathfd, FIONREAD, &val) == -1); +	ATF_REQUIRE_ERRNO(EBADF, ioctl(pathfd, FIONWRITE, &val) == -1); +	ATF_REQUIRE_ERRNO(EBADF, ioctl(pathfd, FIONSPACE, &val) == -1); + +	CHECKED_CLOSE(pathfd); + +	/* Device ioctls should fail. */ +	pathfd = open("/dev/mem", O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	me.me_vaddr = (uintptr_t)&me; +	ATF_REQUIRE_ERRNO(EBADF, ioctl(pathfd, MEM_EXTRACT_PADDR, &me) == -1); + +	CHECKED_CLOSE(pathfd); +} + +ATF_TC_WITHOUT_HEAD(path_lock); +ATF_TC_BODY(path_lock, tc) +{ +	char buf[BUFSIZ], path[PATH_MAX]; +	struct flock flk; +	int fd, pathfd; + +	snprintf(path, sizeof(path), "path_rights.XXXXXX"); +	fd = mkostemp(path, O_SHLOCK); +	ATF_REQUIRE_MSG(fd >= 0, FMT_ERR("mkostemp")); +	memset(buf, 0, sizeof(buf)); +	ATF_REQUIRE_MSG(write(fd, buf, sizeof(buf)) == sizeof(buf), +	    FMT_ERR("write()")); + +	/* Verify that O_EXLOCK is ignored when combined with O_PATH. */ +	pathfd = open(path, O_PATH | O_EXLOCK); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	CHECKED_CLOSE(fd); + +	/* flock(2) is prohibited. */ +	ATF_REQUIRE_ERRNO(EOPNOTSUPP, flock(pathfd, LOCK_SH) == -1); +	ATF_REQUIRE_ERRNO(EOPNOTSUPP, flock(pathfd, LOCK_EX) == -1); +	ATF_REQUIRE_ERRNO(EOPNOTSUPP, flock(pathfd, LOCK_SH | LOCK_NB) == -1); +	ATF_REQUIRE_ERRNO(EOPNOTSUPP, flock(pathfd, LOCK_EX | LOCK_NB) == -1); +	ATF_REQUIRE_ERRNO(EOPNOTSUPP, flock(pathfd, LOCK_UN) == -1); + +	/* fcntl(2) file locks are prohibited. */ +	memset(&flk, 0, sizeof(flk)); +	flk.l_whence = SEEK_CUR; +	ATF_REQUIRE_ERRNO(EBADF, fcntl(pathfd, F_GETLK, &flk) == -1); +	flk.l_type = F_RDLCK; +	ATF_REQUIRE_ERRNO(EBADF, fcntl(pathfd, F_SETLK, &flk) == -1); +	ATF_REQUIRE_ERRNO(EBADF, fcntl(pathfd, F_SETLKW, &flk) == -1); +	flk.l_type = F_WRLCK; +	ATF_REQUIRE_ERRNO(EBADF, fcntl(pathfd, F_SETLK, &flk) == -1); +	ATF_REQUIRE_ERRNO(EBADF, fcntl(pathfd, F_SETLKW, &flk) == -1); + +	CHECKED_CLOSE(pathfd); +} + +/* + * Verify fstatat(AT_EMPTY_PATH) on non-regular dirfd. + * Verify that fstatat(AT_EMPTY_PATH) on NULL path returns EFAULT. + */ +ATF_TC_WITHOUT_HEAD(path_pipe_fstatat); +ATF_TC_BODY(path_pipe_fstatat, tc) +{ +	struct stat sb; +	int fd[2]; + +	ATF_REQUIRE_MSG(pipe(fd) == 0, FMT_ERR("pipe")); +	ATF_REQUIRE_MSG(fstatat(fd[0], "", &sb, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fstatat pipe")); +	ATF_REQUIRE_ERRNO(EFAULT, fstatat(fd[0], NULL, &sb, +	    AT_EMPTY_PATH) == -1); +	CHECKED_CLOSE(fd[0]); +	CHECKED_CLOSE(fd[1]); +} + +/* Verify that we can send an O_PATH descriptor over a unix socket. */ +ATF_TC_WITHOUT_HEAD(path_rights); +ATF_TC_BODY(path_rights, tc) +{ +	char path[PATH_MAX]; +	struct cmsghdr *cmsg; +	struct msghdr msg; +	struct iovec iov; +	int flags, pathfd, pathfd_copy, sd[2]; +	char c; + +	ATF_REQUIRE_MSG(socketpair(PF_LOCAL, SOCK_STREAM, 0, sd) == 0, +	    FMT_ERR("socketpair")); + +	mktfile(path, "path_rights.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	/* Package up the O_PATH and send it over the socket pair. */ +	cmsg = malloc(CMSG_SPACE(sizeof(pathfd))); +	ATF_REQUIRE_MSG(cmsg != NULL, FMT_ERR("malloc")); + +	cmsg->cmsg_len = CMSG_LEN(sizeof(pathfd)); +	cmsg->cmsg_level = SOL_SOCKET; +	cmsg->cmsg_type = SCM_RIGHTS; +	*(int *)(void *)CMSG_DATA(cmsg) = pathfd; + +	c = 0; +	iov.iov_base = &c; +	iov.iov_len = 1; + +	memset(&msg, 0, sizeof(msg)); +	msg.msg_iov = &iov; +	msg.msg_iovlen = 1; +	msg.msg_control = cmsg; +	msg.msg_controllen = CMSG_SPACE(sizeof(pathfd)); + +	ATF_REQUIRE_MSG(sendmsg(sd[0], &msg, 0) == sizeof(c), +	    FMT_ERR("sendmsg")); + +	/* Grab the pathfd copy from the other end of the pair. */ +	memset(&msg, 0, sizeof(msg)); +	msg.msg_iov = &iov; +	msg.msg_iovlen = 1; +	msg.msg_control = cmsg; +	msg.msg_controllen = CMSG_SPACE(sizeof(pathfd)); + +	ATF_REQUIRE_MSG(recvmsg(sd[1], &msg, 0) == 1, +	    FMT_ERR("recvmsg")); +	pathfd_copy = *(int *)(void *)CMSG_DATA(cmsg); +	ATF_REQUIRE_MSG(pathfd_copy != pathfd, +	    "pathfd and pathfd_copy are equal"); + +	/* Verify that the copy has O_PATH properties. */ +	flags = fcntl(pathfd_copy, F_GETFL); +	ATF_REQUIRE_MSG(flags != -1, FMT_ERR("fcntl")); +	ATF_REQUIRE_MSG((flags & O_PATH) != 0, "O_PATH is not set"); +	ATF_REQUIRE_ERRNO(EBADF, +	    read(pathfd_copy, &c, 1) == -1); +	ATF_REQUIRE_ERRNO(EBADF, +	    write(pathfd_copy, &c, 1) == -1); + +	CHECKED_CLOSE(pathfd); +	CHECKED_CLOSE(pathfd_copy); +	CHECKED_CLOSE(sd[0]); +	CHECKED_CLOSE(sd[1]); +} + +/* Verify that a local socket can be opened with O_PATH. */ +ATF_TC_WITHOUT_HEAD(path_unix); +ATF_TC_BODY(path_unix, tc) +{ +	char buf[BUFSIZ], path[PATH_MAX]; +	struct kevent ev; +	struct sockaddr_un sun; +	struct stat sb; +	int kq, pathfd, sd; + +	snprintf(path, sizeof(path), "path_unix.XXXXXX"); +	ATF_REQUIRE_MSG(mktemp(path) == path, FMT_ERR("mktemp")); + +	sd = socket(PF_LOCAL, SOCK_STREAM, 0); +	ATF_REQUIRE_MSG(sd >= 0, FMT_ERR("socket")); + +	memset(&sun, 0, sizeof(sun)); +	sun.sun_family = PF_LOCAL; +	(void)strlcpy(sun.sun_path, path, sizeof(sun.sun_path)); +	ATF_REQUIRE_MSG(bind(sd, (struct sockaddr *)&sun, SUN_LEN(&sun)) == 0, +	    FMT_ERR("bind")); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE_MSG(fstatat(pathfd, "", &sb, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fstatat")); +	ATF_REQUIRE_MSG(sb.st_mode & S_IFSOCK, "socket mode %#x", sb.st_mode); +	ATF_REQUIRE_MSG(sb.st_ino != 0, "socket has inode number 0"); + +	memset(buf, 0, sizeof(buf)); +	ATF_REQUIRE_ERRNO(EBADF, write(pathfd, buf, sizeof(buf))); +	ATF_REQUIRE_ERRNO(EBADF, read(pathfd, buf, sizeof(buf))); + +	/* kevent() is disallowed with sockets. */ +	kq = kqueue(); +	ATF_REQUIRE_MSG(kq >= 0, FMT_ERR("kqueue")); +	EV_SET(&ev, pathfd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0); +	ATF_REQUIRE_ERRNO(EBADF, kevent(kq, &ev, 1, NULL, 0, NULL) == -1); + +	/* Should not be able to open a socket without O_PATH. */ +	ATF_REQUIRE_ERRNO(EOPNOTSUPP, openat(pathfd, "", O_EMPTY_PATH) == -1); + +	ATF_REQUIRE_MSG(funlinkat(AT_FDCWD, path, pathfd, 0) == 0, +	    FMT_ERR("funlinkat")); + +	CHECKED_CLOSE(sd); +	CHECKED_CLOSE(pathfd); +} + +/* + * Check that we can perform operations using an O_PATH fd for an unlinked file. + */ +ATF_TC_WITHOUT_HEAD(path_unlinked); +ATF_TC_BODY(path_unlinked, tc) +{ +	char path[PATH_MAX]; +	struct stat sb; +	int pathfd; + +	mktfile(path, "path_rights.XXXXXX"); + +	pathfd = open(path, O_PATH); +	ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open")); + +	ATF_REQUIRE_MSG(fstatat(pathfd, "", &sb, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fstatat")); +	ATF_REQUIRE(sb.st_nlink == 1); +	ATF_REQUIRE_MSG(fstat(pathfd, &sb) == 0, FMT_ERR("fstat")); +	ATF_REQUIRE(sb.st_nlink == 1); + +	ATF_REQUIRE_MSG(unlink(path) == 0, FMT_ERR("unlink")); + +	ATF_REQUIRE_MSG(fstatat(pathfd, "", &sb, AT_EMPTY_PATH) == 0, +	    FMT_ERR("fstatat")); +	ATF_REQUIRE(sb.st_nlink == 0); +	ATF_REQUIRE_MSG(fstat(pathfd, &sb) == 0, FMT_ERR("fstat")); +	ATF_REQUIRE(sb.st_nlink == 0); + +	CHECKED_CLOSE(pathfd); +} + +ATF_TP_ADD_TCS(tp) +{ +	ATF_TP_ADD_TC(tp, path_access); +	ATF_TP_ADD_TC(tp, path_aio); +	ATF_TP_ADD_TC(tp, path_capsicum); +	ATF_TP_ADD_TC(tp, path_capsicum_empty); +	ATF_TP_ADD_TC(tp, path_coredump); +	ATF_TP_ADD_TC(tp, path_directory); +	ATF_TP_ADD_TC(tp, path_directory_not_root); +	ATF_TP_ADD_TC(tp, path_empty); +	ATF_TP_ADD_TC(tp, path_empty_not_root); +	ATF_TP_ADD_TC(tp, path_empty_root); +	ATF_TP_ADD_TC(tp, path_event); +	ATF_TP_ADD_TC(tp, path_fcntl); +	ATF_TP_ADD_TC(tp, path_fexecve); +	ATF_TP_ADD_TC(tp, path_fifo); +	ATF_TP_ADD_TC(tp, path_funlinkat); +	ATF_TP_ADD_TC(tp, path_io); +	ATF_TP_ADD_TC(tp, path_ioctl); +	ATF_TP_ADD_TC(tp, path_lock); +	ATF_TP_ADD_TC(tp, path_pipe_fstatat); +	ATF_TP_ADD_TC(tp, path_rights); +	ATF_TP_ADD_TC(tp, path_unix); +	ATF_TP_ADD_TC(tp, path_unlinked); + +	return (atf_no_error()); +} | 
