/*
 * Copyright (c) 2010  Philip Guenther <guenther@openbsd.org>
 *
 * Public domain.
 *
 * Verify that mkdtemp() and mkstemps() doesn't overrun or underrun
 * the template buffer and that it can generate names that don't
 * contain any X's
 */

#include <config.h>

#include <sys/mman.h>
#include <sys/stat.h>

#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#define SUDO_ERROR_WRAP 0

#include <sudo_compat.h>
#include <sudo_util.h>
#include <sudo_fatal.h>

#ifndef MAP_ANON
# if defined(MAP_ANONYMOUS)
#  define MAP_ANON MAP_ANONYMOUS
# endif
#endif

#ifndef MAP_FAILED
# define MAP_FAILED ((void *) -1)
#endif

#define MAX_TEMPLATE_LEN	10
#define MAX_TRIES		100
#define MIN_Xs			6

#define SUFFIX	".suff"
#define SLEN	(sizeof SUFFIX - 1)

sudo_dso_public int main(int argc, char *argv[]);

/*
 * verify that a path generated by mkdtemp() or mkstemp() looks like a
 * reasonable expansion of the template and matches the fd.  Returns true
 * if all the X's were replaced with non-X's
 */
static int
check(int fd, char const *kind, char const *path, char const *prefix,
    size_t plen, char const *suffix, size_t slen, size_t tlen)
{
	struct stat sb, fsb;
	char const *p;

	if (tlen < MIN_Xs) {
		if (fd != -1)
			sudo_fatalx("%s(%s) succeed with too few Xs", kind, path);
		if (errno != EINVAL)
			sudo_fatal("%s(%s) failed with wrong errno: %d", kind, path, errno);
		return 1;
	}
	if (fd == -1)
		sudo_fatal("%s(%s)", kind, path);
	if (stat(path, &sb))
		sudo_fatal("%s: stat(%s)", kind, path);
	if (fd >= 0) {
		if (fstat(fd, &fsb))
			sudo_fatal("%s: fstat(%d==%s)", kind, fd, path);
		if (sb.st_dev != fsb.st_dev || sb.st_ino != fsb.st_ino)
			sudo_fatalx("%s: stat mismatch", kind);
	}
	if (memcmp(path, prefix, plen) != 0)
		sudo_fatalx("%s: prefix changed!  %s vs %s", kind, prefix, path);
	if (memcmp(path + plen + tlen, suffix, slen + 1) != 0)
		sudo_fatalx("%s: suffix changed!  %s vs %s", kind, suffix, path);
	for (p = path + plen; p < path + plen + tlen; p++)
		if (*p == '\0')
			sudo_fatalx("%s: unexpected truncation", kind);
		else if (*p == 'X')
			return 0;
	return 1;
}

static void
try_mkdtemp(char *p, char const *prefix, size_t len)
{
	size_t plen = strlen(prefix);
	int fd, tries, ok;

	for (tries = 0; tries < MAX_TRIES; tries++) {
		memcpy(p, prefix, plen);
		memset(p + plen, 'X', len);
		p[plen + len] = '\0';
		fd = mkdtemp(p) ? -2 : -1;
		ok = check(fd, "mkdtemp", p, prefix, plen, "", 0, len);
		rmdir(p);
		if (ok)
			return;
	}
	sudo_fatalx("mkdtemp: exceeded MAX_TRIES");
}

static void
try_mkstemps(char *p, char const *prefix, size_t len, char const *suffix)
{
	size_t plen = strlen(prefix);
	size_t slen = strlen(suffix);
	int tries, fd, ok;

	for (tries = 0; tries < MAX_TRIES; tries++) {
		memcpy(p, prefix, plen);
		memset(p + plen, 'X', len);
		memcpy(p + plen + len, suffix, slen + 1);
		fd = mkstemps(p, (int)slen);
		ok = check(fd, "mkstemp", p, prefix, plen, suffix, slen, len);
		close(fd);
		unlink(p);
		if (ok)
			return;
	}
	sudo_fatalx("mkstemps: exceeded MAX_TRIES");
}

int
main(int argc, char *argv[])
{
	char cwd[PATH_MAX + 1];
	char *p;
	size_t clen, i;
	size_t pg;
	int ch;

	initprogname(argc > 0 ? argv[0] : "mktemp_test");

	while ((ch = getopt(argc, argv, "v")) != -1) {
	    switch (ch) {
	    case 'v':
		/* ignore */
		break;
	    default:
		fprintf(stderr, "usage: %s [-v]\n", getprogname());
		return EXIT_FAILURE;
	    }
	}
	argc -= optind;
	argv += optind;

	pg = (size_t)sysconf(_SC_PAGESIZE);
	if (pg == (size_t)-1)
		sudo_fatal("sysconf(_SC_PAGESIZE)");
	if (getcwd(cwd, sizeof cwd - 1) == NULL)
		sudo_fatal("getcwd");
	clen = strlen(cwd);
	cwd[clen++] = '/';
	cwd[clen] = '\0';
#ifdef MAP_ANON
	p = mmap(NULL, pg * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
#else
	ch = open("/dev/zero", O_RDWR);
	if (ch == -1)
		sudo_fatal("/dev/zero");
	p = mmap(NULL, pg * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE, ch, 0);
#endif
	if (p == MAP_FAILED)
		sudo_fatal("mmap");
	if (mprotect(p, pg, PROT_NONE) || mprotect(p + pg * 2, pg, PROT_NONE))
		sudo_fatal("mprotect");
	p += pg;

	for (i = MAX_TEMPLATE_LEN; i != 0; i--) {
		/* try first at the start of a page, no prefix */
		try_mkdtemp(p, "", i);
		/* now at the end of the page, no prefix */
		try_mkdtemp(p + pg - i - 1, "", i);
		/* start of the page, prefixed with the cwd */
		try_mkdtemp(p, cwd, i);
		/* how about at the end of the page, prefixed with cwd? */
		try_mkdtemp(p + pg - clen - i - 1, cwd, i);

		/* again, with mkstemps() and an empty suffix */
		/* try first at the start of a page, no prefix */
		try_mkstemps(p, "", i, "");
		/* now at the end of the page, no prefix */
		try_mkstemps(p + pg - i - 1, "", i, "");
		/* start of the page, prefixed with the cwd */
		try_mkstemps(p, cwd, i, "");
		/* how about at the end of the page, prefixed with cwd? */
		try_mkstemps(p + pg - clen - i - 1, cwd, i, "");

		/* mkstemps() and a non-empty suffix */
		/* try first at the start of a page, no prefix */
		try_mkstemps(p, "", i, SUFFIX);
		/* now at the end of the page, no prefix */
		try_mkstemps(p + pg - i - SLEN - 1, "", i, SUFFIX);
		/* start of the page, prefixed with the cwd */
		try_mkstemps(p, cwd, i, SUFFIX);
		/* how about at the end of the page, prefixed with cwd? */
		try_mkstemps(p + pg - clen - i - SLEN - 1, cwd, i, SUFFIX);
	}

	return 0;
}
