// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2019 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <darrick.wong@oracle.com>
 *
 * Test program to open unlinked files and leak them.
 */
#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif
#include <time.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include "global.h"

static int min_fd = -1;
static int max_fd = -1;
static unsigned int nr_opened = 0;
static float start_time;
static int shutdown_fd = -1;

void clock_time(float *time)
{
	static clockid_t clkid = CLOCK_MONOTONIC;
	struct timespec ts;
	int ret;

retry:
	ret = clock_gettime(clkid, &ts);
	if (ret) {
		if (clkid == CLOCK_MONOTONIC) {
			clkid = CLOCK_REALTIME;
			goto retry;
		}
		perror("clock_gettime");
		exit(2);
	}
	*time = ts.tv_sec + ((float)ts.tv_nsec / 1000000000);
}

/*
 * Exit the program due to an error.
 *
 * If we've exhausted all the file descriptors, make sure we close all the
 * open fds in the order we received them in order to exploit a quirk of ext4
 * and xfs where the oldest unlinked inodes are at the /end/ of the unlinked
 * lists, which will make removing the unlinked files maximally painful.
 *
 * If it's some other error, just die and let the kernel sort it out.
 */
void die(void)
{
	float end_time;
	int fd;

	switch (errno) {
	case EMFILE:
	case ENFILE:
	case ENOSPC:
		clock_time(&end_time);
		printf("Opened %u files in %.2fs.\n", nr_opened,
				end_time - start_time);
		fflush(stdout);

		if (shutdown_fd >= 0) {
			/*
			 * Flush the log so that we have to process the
			 * unlinked inodes the next time we mount.
			 */
			int flag = XFS_FSOP_GOING_FLAGS_LOGFLUSH;
			int ret;

			ret = ioctl(shutdown_fd, XFS_IOC_GOINGDOWN, &flag);
			if (ret) {
				perror("shutdown");
				exit(2);
			}
			exit(0);
		}

		clock_time(&start_time);
		for (fd = min_fd; fd <= max_fd; fd++)
			close(fd);
		clock_time(&end_time);
		printf("Closed %u files in %.2fs.\n", nr_opened,
				end_time - start_time);
		exit(0);
		break;
	default:
		perror("open?");
		exit(2);
		break;
	}
}

/* Remember how many file we open and all that. */
void remember_fd(int fd)
{
	if (min_fd == -1 || min_fd > fd)
		min_fd = fd;
	if (max_fd == -1 || max_fd < fd)
		max_fd = fd;
	nr_opened++;
}

/* Put an opened file on the unlinked list and leak the fd. */
void leak_tmpfile(void)
{
	int fd = -1;
	int ret;
#ifdef O_TMPFILE
	static int try_o_tmpfile = 1;
#endif

	/* Try to create an O_TMPFILE and leak the fd. */
#ifdef O_TMPFILE
	if (try_o_tmpfile) {
		fd = open(".", O_TMPFILE | O_RDWR, 0644);
		if (fd >= 0) {
			remember_fd(fd);
			return;
		}
		if (fd < 0) {
			if (errno == EOPNOTSUPP)
				try_o_tmpfile = 0;
			else
				die();
		}
	}
#endif

	/* Oh well, create a new file, unlink it, and leak the fd. */
	fd = open("./moo", O_CREAT | O_RDWR, 0644);
	if (fd < 0)
		die();
	ret = unlink("./moo");
	if (ret)
		die();
	remember_fd(fd);
}

/*
 * Try to put as many files on the unlinked list and then kill them.
 * The first argument is a directory to chdir into; the second argumennt (if
 * provided) is a file path that will be opened and then used to shut down the
 * fs before the program exits.
 */
int main(int argc, char *argv[])
{
	int ret;

	if (argc > 1) {
		ret = chdir(argv[1]);
		if (ret)
			perror(argv[1]);
	}
	if (argc > 2) {
		shutdown_fd = open(argv[2], O_RDONLY);
		if (shutdown_fd < 0) {
			perror(argv[2]);
			return 1;
		}
	}

	clock_time(&start_time);
	while (1)
		leak_tmpfile();
	return 0;
}
