/* SPDX-License-Identifier: LGPL-2.1+ */
/*
 * Copyright (c) 2021 Christian Brauner <christian.brauner@ubuntu.com>
 * All Rights Reserved.
 *
 * Regression test to verify that creating a series of detached mounts,
 * attaching them to the filesystem, and unmounting them does not trigger an
 * integer overflow in ns->mounts causing the kernel to block any new mounts in
 * count_mounts() and returning ENOSPC because it falsely assumes that the
 * maximum number of mounts in the mount namespace has been reached, i.e. it
 * thinks it can't fit the new mounts into the mount namespace anymore.
 */

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#include "idmapped-mounts/missing.h"

static bool is_shared_mountpoint(const char *path)
{
	bool shared = false;
	FILE *f = NULL;
	char *line = NULL;
	int i;
	size_t len = 0;

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

	while (getline(&line, &len, f) > 0) {
		char *slider1, *slider2;

		for (slider1 = line, i = 0; slider1 && i < 4; i++)
			slider1 = strchr(slider1 + 1, ' ');

		if (!slider1)
			continue;

		slider2 = strchr(slider1 + 1, ' ');
		if (!slider2)
			continue;

		*slider2 = '\0';
		if (strcmp(slider1 + 1, path) == 0) {
			/* This is the path. Is it shared? */
			slider1 = strchr(slider2 + 1, ' ');
			if (slider1 && strstr(slider1, "shared:")) {
				shared = true;
				break;
			}
		}
	}
	fclose(f);
	free(line);

	return shared;
}

static void usage(void)
{
	const char *text = "mount-new [--recursive] <base-dir>\n";
	fprintf(stderr, "%s", text);
	_exit(EXIT_SUCCESS);
}

#define exit_usage(format, ...)                              \
	({                                                   \
		fprintf(stderr, format "\n", ##__VA_ARGS__); \
		usage();                                     \
	})

#define exit_log(format, ...)                                \
	({                                                   \
		fprintf(stderr, format "\n", ##__VA_ARGS__); \
		exit(EXIT_FAILURE);                          \
	})

static const struct option longopts[] = {
	{"help",	no_argument,		0,	'a'},
	{ NULL,		no_argument,		0,	 0 },
};

int main(int argc, char *argv[])
{
	int exit_code = EXIT_SUCCESS, index = 0;
	int dfd, fd_tree, new_argc, ret, i;
	char *base_dir;
	char *const *new_argv;
	char target[PATH_MAX];

	while ((ret = getopt_long_only(argc, argv, "", longopts, &index)) != -1) {
		switch (ret) {
		case 'a':
			/* fallthrough */
		default:
			usage();
		}
	}

	new_argv = &argv[optind];
	new_argc = argc - optind;
	if (new_argc < 1)
		exit_usage("Missing base directory\n");
	base_dir = new_argv[0];

	if (*base_dir != '/')
		exit_log("Please specify an absolute path");

	/* Ensure that target is a shared mountpoint. */
	if (!is_shared_mountpoint(base_dir))
		exit_log("Please ensure that \"%s\" is a shared mountpoint", base_dir);

	ret = unshare(CLONE_NEWNS);
	if (ret < 0)
		exit_log("%m - Failed to create new mount namespace");

	ret = mount(NULL, base_dir, NULL, MS_REC | MS_SHARED, NULL);
	if (ret < 0)
		exit_log("%m - Failed to make base_dir shared mountpoint");

	dfd = open(base_dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
	if (dfd < 0)
		exit_log("%m - Failed to open base directory \"%s\"", base_dir);

	ret = mkdirat(dfd, "detached-move-mount", 0755);
	if (ret < 0)
		exit_log("%m - Failed to create required temporary directories");

	ret = snprintf(target, sizeof(target), "%s/detached-move-mount", base_dir);
	if (ret < 0 || (size_t)ret >= sizeof(target))
		exit_log("%m - Failed to assemble target path");

	/*
	 * Having a mount table with 10000 mounts is already quite excessive
	 * and shoult account even for weird test systems.
	 */
	for (i = 0; i < 10000; i++) {
		fd_tree = sys_open_tree(dfd, "detached-move-mount",
					OPEN_TREE_CLONE |
					OPEN_TREE_CLOEXEC |
					AT_EMPTY_PATH);
		if (fd_tree < 0) {
			if (errno == ENOSYS) /* New mount API not (fully) supported. */
				break;

			fprintf(stderr, "%m - Failed to open %d(detached-move-mount)", dfd);
			exit_code = EXIT_FAILURE;
			break;
		}

		ret = sys_move_mount(fd_tree, "", dfd, "detached-move-mount", MOVE_MOUNT_F_EMPTY_PATH);
		if (ret < 0) {
			if (errno == ENOSPC)
				fprintf(stderr, "%m - Buggy mount counting");
			else if (errno == ENOSYS) /* New mount API not (fully) supported. */
				break;
			else
				fprintf(stderr, "%m - Failed to attach mount to %d(detached-move-mount)", dfd);
			exit_code = EXIT_FAILURE;
			break;
		}
		close(fd_tree);

		ret = umount2(target, MNT_DETACH);
		if (ret < 0) {
			fprintf(stderr, "%m - Failed to unmount %s", target);
			exit_code = EXIT_FAILURE;
			break;
		}
	}

	(void)unlinkat(dfd, "detached-move-mount", AT_REMOVEDIR);
	close(dfd);

	exit(exit_code);
}
