/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org>
 */

#include <sys/param.h>
#include <sys/jail.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/uio.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <jail.h>
#include <stdio.h>
#include <stdlib.h>

#include <atf-c.h>

static void
build_iovec(struct iovec **iov, int *iovlen, const char *name, void *val,
    size_t len)
{
	int i;

	if (*iovlen < 0)
		return;
	i = *iovlen;
	*iov = realloc(*iov, sizeof **iov * (i + 2));
	if (*iov == NULL) {
		*iovlen = -1;
		return;
	}
	(*iov)[i].iov_base = strdup(name);
	(*iov)[i].iov_len = strlen(name) + 1;
	i++;
	(*iov)[i].iov_base = val;
	if (len == (size_t)-1) {
		if (val != NULL)
			len = strlen(val) + 1;
		else
			len = 0;
	}
	(*iov)[i].iov_len = (int)len;
	*iovlen = ++i;
}

static void
free_iovec(struct iovec **iov, int *iovlen)
{
	int i;

	for (i = 0; i < *iovlen; i += 2)
		free((*iov)[i].iov_base);
	free(*iov);
}

static void
mkdir_checked(const char *dir, mode_t mode)
{
	int error;

	error = mkdir(dir, mode);
	ATF_REQUIRE_MSG(error == 0 || errno == EEXIST,
	    "mkdir %s: %s", dir, strerror(errno));
}

static void __unused
mount_nullfs(const char *dir, const char *target)
{
	struct iovec *iov;
	char errmsg[1024];
	int error, iovlen;

	iov = NULL;
	iovlen = 0;

	build_iovec(&iov, &iovlen, __DECONST(char *, "fstype"),
	    __DECONST(char *, "nullfs"), (size_t)-1);
	build_iovec(&iov, &iovlen, __DECONST(char *, "fspath"),
	    __DECONST(char *, target), (size_t)-1);
	build_iovec(&iov, &iovlen, __DECONST(char *, "from"),
	    __DECONST(char *, dir), (size_t)-1);
	build_iovec(&iov, &iovlen, __DECONST(char *, "errmsg"),
	    errmsg, sizeof(errmsg));

	errmsg[0] = '\0';
	error = nmount(iov, iovlen, 0);
	ATF_REQUIRE_MSG(error == 0, "nmount: %s",
	    errmsg[0] != '\0' ? errmsg : strerror(errno));

	free_iovec(&iov, &iovlen);
}

ATF_TC_WITH_CLEANUP(jail_root);
ATF_TC_HEAD(jail_root, tc)
{
	atf_tc_set_md_var(tc, "require.user", "root");
}
ATF_TC_BODY(jail_root, tc)
{
	int error, fd, jid;

	mkdir_checked("./root", 0755);
	mkdir_checked("./root/a", 0755);
	mkdir_checked("./root/b", 0755);
	mkdir_checked("./root/a/c", 0755);

	jid = jail_setv(JAIL_CREATE | JAIL_ATTACH,
	    "name", "nullfs_jail_root_test",
	    "allow.mount", "true",
	    "allow.mount.nullfs", "true",
	    "enforce_statfs", "1",
	    "path", "./root",
	    "persist", NULL,
	    NULL);
	ATF_REQUIRE_MSG(jid >= 0, "jail_setv: %s", jail_errmsg);

	mount_nullfs("/a", "/b");

	error = chdir("/b/c");
	ATF_REQUIRE(error == 0);

	error = rename("/a/c", "/c");
	ATF_REQUIRE(error == 0);

	/* Descending to the jail root should be ok. */
	error = chdir("..");
	ATF_REQUIRE(error == 0);

	/* Going beyond the root will trigger an error. */
	error = chdir("..");
	ATF_REQUIRE_ERRNO(ENOENT, error != 0);
	fd = open("..", O_RDONLY | O_DIRECTORY);
	ATF_REQUIRE_ERRNO(ENOENT, fd < 0);
}
ATF_TC_CLEANUP(jail_root, tc)
{
	struct statfs fs;
	fsid_t fsid;
	int error, jid;

	error = statfs("./root/b", &fs);
	if (error != 0)
		err(1, "statfs ./b");
	fsid = fs.f_fsid;
	error = statfs("./root", &fs);
	if (error != 0)
		err(1, "statfs ./root");
	if (fsid.val[0] != fs.f_fsid.val[0] ||
	    fsid.val[1] != fs.f_fsid.val[1]) {
		error = unmount("./root/b", 0);
		if (error != 0)
			err(1, "unmount ./root/b");
	}

	jid = jail_getid("nullfs_jail_root_test");
	if (jid >= 0) {
		error = jail_remove(jid);
		if (error != 0)
			err(1, "jail_remove");
	}
}

ATF_TP_ADD_TCS(tp)
{
	ATF_TP_ADD_TC(tp, jail_root);
	return (atf_no_error());
}
