// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdarg.h>
#include <sys/syscall.h>

#include "../kselftest_harness.h"

#ifndef CLONE_NEWNS
#define CLONE_NEWNS 0x00020000
#endif

#ifndef CLONE_NEWUSER
#define CLONE_NEWUSER 0x10000000
#endif

#ifndef MS_SHARED
#define MS_SHARED (1 << 20)
#endif

#ifndef MS_PRIVATE
#define MS_PRIVATE (1<<18)
#endif

#ifndef MOVE_MOUNT_SET_GROUP
#define MOVE_MOUNT_SET_GROUP 0x00000100
#endif

#ifndef MOVE_MOUNT_F_EMPTY_PATH
#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
#endif

#ifndef MOVE_MOUNT_T_EMPTY_PATH
#define MOVE_MOUNT_T_EMPTY_PATH 0x00000040
#endif

static ssize_t write_nointr(int fd, const void *buf, size_t count)
{
	ssize_t ret;

	do {
		ret = write(fd, buf, count);
	} while (ret < 0 && errno == EINTR);

	return ret;
}

static int write_file(const char *path, const void *buf, size_t count)
{
	int fd;
	ssize_t ret;

	fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
	if (fd < 0)
		return -1;

	ret = write_nointr(fd, buf, count);
	close(fd);
	if (ret < 0 || (size_t)ret != count)
		return -1;

	return 0;
}

static int create_and_enter_userns(void)
{
	uid_t uid;
	gid_t gid;
	char map[100];

	uid = getuid();
	gid = getgid();

	if (unshare(CLONE_NEWUSER))
		return -1;

	if (write_file("/proc/self/setgroups", "deny", sizeof("deny") - 1) &&
	    errno != ENOENT)
		return -1;

	snprintf(map, sizeof(map), "0 %d 1", uid);
	if (write_file("/proc/self/uid_map", map, strlen(map)))
		return -1;


	snprintf(map, sizeof(map), "0 %d 1", gid);
	if (write_file("/proc/self/gid_map", map, strlen(map)))
		return -1;

	if (setgid(0))
		return -1;

	if (setuid(0))
		return -1;

	return 0;
}

static int prepare_unpriv_mountns(void)
{
	if (create_and_enter_userns())
		return -1;

	if (unshare(CLONE_NEWNS))
		return -1;

	if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, 0))
		return -1;

	return 0;
}

static char *get_field(char *src, int nfields)
{
	int i;
	char *p = src;

	for (i = 0; i < nfields; i++) {
		while (*p && *p != ' ' && *p != '\t')
			p++;

		if (!*p)
			break;

		p++;
	}

	return p;
}

static void null_endofword(char *word)
{
	while (*word && *word != ' ' && *word != '\t')
		word++;
	*word = '\0';
}

static bool is_shared_mount(const char *path)
{
	size_t len = 0;
	char *line = NULL;
	FILE *f = NULL;

	f = fopen("/proc/self/mountinfo", "re");
	if (!f)
		return false;

	while (getline(&line, &len, f) != -1) {
		char *opts, *target;

		target = get_field(line, 4);
		if (!target)
			continue;

		opts = get_field(target, 2);
		if (!opts)
			continue;

		null_endofword(target);

		if (strcmp(target, path) != 0)
			continue;

		null_endofword(opts);
		if (strstr(opts, "shared:"))
			return true;
	}

	free(line);
	fclose(f);

	return false;
}

/* Attempt to de-conflict with the selftests tree. */
#ifndef SKIP
#define SKIP(s, ...)	XFAIL(s, ##__VA_ARGS__)
#endif

#define SET_GROUP_FROM	"/tmp/move_mount_set_group_supported_from"
#define SET_GROUP_TO	"/tmp/move_mount_set_group_supported_to"

static bool move_mount_set_group_supported(void)
{
	int ret;

	if (mount("testing", "/tmp", "tmpfs", MS_NOATIME | MS_NODEV,
		  "size=100000,mode=700"))
		return -1;

	if (mount(NULL, "/tmp", NULL, MS_PRIVATE, 0))
		return -1;

	if (mkdir(SET_GROUP_FROM, 0777))
		return -1;

	if (mkdir(SET_GROUP_TO, 0777))
		return -1;

	if (mount("testing", SET_GROUP_FROM, "tmpfs", MS_NOATIME | MS_NODEV,
		  "size=100000,mode=700"))
		return -1;

	if (mount(SET_GROUP_FROM, SET_GROUP_TO, NULL, MS_BIND, NULL))
		return -1;

	if (mount(NULL, SET_GROUP_FROM, NULL, MS_SHARED, 0))
		return -1;

	ret = syscall(__NR_move_mount, AT_FDCWD, SET_GROUP_FROM,
		      AT_FDCWD, SET_GROUP_TO, MOVE_MOUNT_SET_GROUP);
	umount2("/tmp", MNT_DETACH);

	return ret >= 0;
}

FIXTURE(move_mount_set_group) {
};

#define SET_GROUP_A "/tmp/A"

FIXTURE_SETUP(move_mount_set_group)
{
	bool ret;

	ASSERT_EQ(prepare_unpriv_mountns(), 0);

	ret = move_mount_set_group_supported();
	ASSERT_GE(ret, 0);
	if (!ret)
		SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");

	umount2("/tmp", MNT_DETACH);

	ASSERT_EQ(mount("testing", "/tmp", "tmpfs", MS_NOATIME | MS_NODEV,
			"size=100000,mode=700"), 0);

	ASSERT_EQ(mkdir(SET_GROUP_A, 0777), 0);

	ASSERT_EQ(mount("testing", SET_GROUP_A, "tmpfs", MS_NOATIME | MS_NODEV,
			"size=100000,mode=700"), 0);
}

FIXTURE_TEARDOWN(move_mount_set_group)
{
	bool ret;

	ret = move_mount_set_group_supported();
	ASSERT_GE(ret, 0);
	if (!ret)
		SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");

	umount2("/tmp", MNT_DETACH);
}

#define __STACK_SIZE (8 * 1024 * 1024)
static pid_t do_clone(int (*fn)(void *), void *arg, int flags)
{
	void *stack;

	stack = malloc(__STACK_SIZE);
	if (!stack)
		return -ENOMEM;

#ifdef __ia64__
	return __clone2(fn, stack, __STACK_SIZE, flags | SIGCHLD, arg, NULL);
#else
	return clone(fn, stack + __STACK_SIZE, flags | SIGCHLD, arg, NULL);
#endif
}

static int wait_for_pid(pid_t pid)
{
	int status, ret;

again:
	ret = waitpid(pid, &status, 0);
	if (ret == -1) {
		if (errno == EINTR)
			goto again;

		return -1;
	}

	if (!WIFEXITED(status))
		return -1;

	return WEXITSTATUS(status);
}

struct child_args {
	int unsfd;
	int mntnsfd;
	bool shared;
	int mntfd;
};

static int get_nestedns_mount_cb(void *data)
{
	struct child_args *ca = (struct child_args *)data;
	int ret;

	ret = prepare_unpriv_mountns();
	if (ret)
		return 1;

	if (ca->shared) {
		ret = mount(NULL, SET_GROUP_A, NULL, MS_SHARED, 0);
		if (ret)
			return 1;
	}

	ret = open("/proc/self/ns/user", O_RDONLY);
	if (ret < 0)
		return 1;
	ca->unsfd = ret;

	ret = open("/proc/self/ns/mnt", O_RDONLY);
	if (ret < 0)
		return 1;
	ca->mntnsfd = ret;

	ret = open(SET_GROUP_A, O_RDONLY);
	if (ret < 0)
		return 1;
	ca->mntfd = ret;

	return 0;
}

TEST_F(move_mount_set_group, complex_sharing_copying)
{
	struct child_args ca_from = {
		.shared = true,
	};
	struct child_args ca_to = {
		.shared = false,
	};
	pid_t pid;
	bool ret;

	ret = move_mount_set_group_supported();
	ASSERT_GE(ret, 0);
	if (!ret)
		SKIP(return, "move_mount(MOVE_MOUNT_SET_GROUP) is not supported");

	pid = do_clone(get_nestedns_mount_cb, (void *)&ca_from, CLONE_VFORK |
		       CLONE_VM | CLONE_FILES); ASSERT_GT(pid, 0);
	ASSERT_EQ(wait_for_pid(pid), 0);

	pid = do_clone(get_nestedns_mount_cb, (void *)&ca_to, CLONE_VFORK |
		       CLONE_VM | CLONE_FILES); ASSERT_GT(pid, 0);
	ASSERT_EQ(wait_for_pid(pid), 0);

	ASSERT_EQ(syscall(__NR_move_mount, ca_from.mntfd, "",
			  ca_to.mntfd, "", MOVE_MOUNT_SET_GROUP
			  | MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH),
		  0);

	ASSERT_EQ(setns(ca_to.mntnsfd, CLONE_NEWNS), 0);
	ASSERT_EQ(is_shared_mount(SET_GROUP_A), 1);
}

TEST_HARNESS_MAIN
