/*
 * Copyright (C) 2017  Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it would be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define TST_NO_DEFAULT_MAIN
#include "tst_test.h"
#include "lapi/fallocate.h"
#include "lapi/fcntl.h"
#include "lapi/memfd.h"

#include "lapi/syscalls.h"

#include "memfd_create_common.h"

int sys_memfd_create(const char *name, unsigned int flags)
{
	return tst_syscall(__NR_memfd_create, name, flags);
}

int check_fallocate(const char *filename, const int lineno, int fd,
			int mode, off_t offset, off_t len)
{
	int r;

	r = fallocate(fd, mode, offset, len);
	if (r < 0) {
		tst_brk_(filename, lineno, TFAIL | TERRNO,
			"fallocate(%d, %d, %ld, %ld) failed", fd, mode,
			offset, len);
	}

	tst_res_(filename, lineno, TPASS,
		"fallocate(%d, %d, %ld, %ld) succeeded", fd, mode,
		offset, len);

	return r;
}

int check_fallocate_fail(const char *filename, const int lineno, int fd,
				int mode, off_t offset, off_t len)
{
	int r;

	r = fallocate(fd, mode, offset, len);
	if (r >= 0) {
		tst_res_(filename, lineno, TFAIL,
			"fallocate(%d, %d, %ld, %ld) succeeded unexpectedly",
			fd, mode, offset, len);

		return r;
	}

	tst_res_(filename, lineno, TPASS | TERRNO,
		"fallocate(%d, %d, %ld, %ld) failed as expected", fd,
		mode, offset, len);

	return r;
}

void check_ftruncate(const char *filename, const int lineno, int fd,
			off_t length)
{
	safe_ftruncate(filename, lineno, fd, length);

	tst_res_(filename, lineno, TPASS, "ftruncate(%d, %ld) succeeded", fd,
		length);
}

void check_ftruncate_fail(const char *filename, const int lineno,
				int fd, off_t length)
{
	if (ftruncate(fd, length) >= 0) {
		tst_res_(filename, lineno, TFAIL,
			"ftruncate(%d, %ld) succeeded unexpectedly",
			fd, length);

		return;
	}

	tst_res_(filename, lineno, TPASS | TERRNO,
		"ftruncate(%d, %ld) failed as expected", fd, length);
}

int get_mfd_all_available_flags(const char *filename, const int lineno)
{
	unsigned int i;
	int flag;
	int flags2test[] = FLAGS_ALL_ARRAY_INITIALIZER;
	int flags_available = 0;

	if (!MFD_FLAGS_AVAILABLE(0)) {
		tst_brk_(filename, lineno, TCONF,
				"memfd_create(0) not implemented");
	}

	for (i = 0; i < ARRAY_SIZE(flags2test); i++) {
		flag = flags2test[i];

		if (MFD_FLAGS_AVAILABLE(flag))
			flags_available |= flag;
	}

	return flags_available;
}

int mfd_flags_available(const char *filename, const int lineno,
		unsigned int flags)
{
	TEST(sys_memfd_create("dummy_call", flags));
	if (TEST_RETURN < 0) {
		if (TEST_ERRNO != EINVAL) {
			tst_brk_(filename, lineno, TBROK | TTERRNO,
					"memfd_create() failed");
		}

		return 0;
	}

	SAFE_CLOSE(TEST_RETURN);

	return 1;
}

int check_mfd_new(const char *filename, const int lineno,
			const char *name, loff_t sz, int flags)
{
	int fd;

	fd = sys_memfd_create(name, flags);
	if (fd < 0) {
		tst_brk_(filename, lineno, TBROK | TERRNO,
			"memfd_create(%s, %d) failed", name, flags);
	}

	tst_res_(filename, lineno, TPASS, "memfd_create(%s, %d) succeeded",
		name, flags);

	check_ftruncate(filename, lineno, fd, sz);

	return fd;
}

void check_mfd_fail_new(const char *filename, const int lineno,
			const char *name, int flags)
{
	int fd;

	fd = sys_memfd_create(name, flags);
	if (fd >= 0) {
		safe_close(filename, lineno, NULL, fd);
		tst_brk_(filename, lineno, TFAIL,
			 "memfd_create(%s, %d) succeeded unexpectedly",
			name, flags);
	}

	tst_res_(filename, lineno, TPASS | TERRNO,
		"memfd_create(%s, %d) failed as expected", name, flags);
}

void *check_mmap(const char *file, const int lineno, void *addr, size_t length,
		int prot, int flags, int fd, off_t offset)
{
	void *p;

	p = safe_mmap(file, lineno, addr, length, prot, flags, fd, offset);

	tst_res_(file, lineno, TPASS,
		"mmap(%p, %zu, %i, %i, %i, %zi) succeeded", addr,
		length, prot, flags, fd, offset);

	return p;
}

void check_mmap_fail(const char *file, const int lineno, void *addr,
		size_t length, int prot, int flags, int fd, off_t offset)
{
	if (mmap(addr, length, prot, flags, fd, offset) != MAP_FAILED) {
		safe_munmap(file, lineno, NULL, addr, length);
		tst_res_(file, lineno, TFAIL,
			"mmap(%p, %zu, %i, %i, %i, %zi) succeeded unexpectedly",
			addr, length, prot, flags, fd, offset);

		return;
	}

	tst_res_(file, lineno, TPASS | TERRNO,
		"mmap(%p, %zu, %i, %i, %i, %zi) failed as expected",
		addr, length, prot, flags, fd, offset);
}

void check_munmap(const char *file, const int lineno, void *p, size_t length)
{
	safe_munmap(file, lineno, NULL, p, length);

	tst_res_(file, lineno, TPASS, "munmap(%p, %ld) succeeded", p, length);
}

void check_mfd_has_seals(const char *file, const int lineno, int fd, int seals)
{
	int ret = SAFE_FCNTL((fd), F_GET_SEALS);
	if (ret	!= seals) {
		tst_brk_(file, lineno, TFAIL,
			"fd %d doesn't have expected seals (%d expected %d)",
			fd, ret, seals);
	}

	tst_res_(file, lineno, TPASS,
		 "fd %d has expected seals (%d)", fd, seals);
}

void check_mprotect(const char *file, const int lineno, void *addr,
		size_t length, int prot)
{
	if (mprotect(addr, length, prot) < 0) {
		tst_brk_(file, lineno, TFAIL | TERRNO,
			"mprotect(%p, %ld, %d) failed", addr, length, prot);
	}

	tst_res_(file, lineno, TPASS, "mprotect(%p, %ld, %d) succeeded", addr,
		length, prot);
}

void check_mfd_fail_add_seals(const char *filename, const int lineno,
				int fd, int seals)
{
	if (fcntl(fd, F_ADD_SEALS, seals) >= 0) {
		tst_brk_(filename, lineno, TFAIL,
			"fcntl(%d, F_ADD_SEALS) succeeded unexpectedly", fd);
	}

	tst_res_(filename, lineno, TPASS | TERRNO,
		"fcntl(%d, F_ADD_SEALS, %d) failed as expected", (fd),
		(seals));
}

void check_mfd_size(const char *filename, const int lineno, int fd,
			size_t size)
{
	struct stat st;

	safe_fstat(filename, lineno, fd, &st);

	if (st.st_size != (long)size) {
		tst_brk_(filename, lineno, TFAIL,
			"fstat(%d, &st): unexpected file size", fd);
	}

	tst_res_(filename, lineno, TPASS,
		"fstat(%d, &st): file size is correct", fd);
}

int check_mfd_open(const char *filename, const int lineno, int fd,
			int flags, mode_t mode)
{
	int r;
	char buf[512];

	sprintf(buf, "/proc/self/fd/%d", fd);

	r = safe_open(filename, lineno, NULL, buf, flags, mode);

	tst_res_(filename, lineno, TPASS, "open(%s, %d, %d) succeeded", buf,
		flags, mode);

	return r;
}

void check_mfd_fail_open(const char *filename, const int lineno, int fd,
				int flags, mode_t mode)
{
	char buf[512];

	sprintf(buf, "/proc/self/fd/%d", fd);

	fd = open(buf, flags, mode);
	if (fd > 0) {
		safe_close(filename, lineno, NULL, fd);
		tst_res_(filename, lineno, TFAIL,
			"open(%s, %d, %d) succeeded unexpectedly", buf,
			flags, mode);
	} else {
		tst_res_(filename, lineno, TPASS | TERRNO,
			"open(%s, %d, %d) failed as expected", buf,
			flags, mode);
	}
}

void check_mfd_readable(const char *filename, const int lineno, int fd)
{
	char buf[16];
	void *p;

	safe_read(filename, lineno, NULL, 1, fd, buf, sizeof(buf));
	tst_res_(filename, lineno, TPASS, "read(%d, %s, %ld) succeeded", fd,
		buf, sizeof(buf));

	/* verify PROT_READ *is* allowed */
	p = check_mmap(filename, lineno, NULL, MFD_DEF_SIZE, PROT_READ,
			MAP_PRIVATE, fd, 0);

	check_munmap(filename, lineno, p, MFD_DEF_SIZE);

	/* verify MAP_PRIVATE is *always* allowed (even writable) */
	p = check_mmap(filename, lineno, NULL, MFD_DEF_SIZE,
			PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);

	check_munmap(filename, lineno, p, MFD_DEF_SIZE);
}

void check_mfd_writeable(const char *filename, const int lineno, int fd)
{
	void *p;

	/* verify write() succeeds */
	safe_write(filename, lineno, NULL, 1, fd, "\0\0\0\0", 4);
	tst_res_(filename, lineno, TPASS, "write(%d, %s, %d) succeeded", fd,
		"\\0\\0\\0\\0", 4);

	/* verify PROT_READ | PROT_WRITE is allowed */
	p = check_mmap(filename, lineno, NULL, MFD_DEF_SIZE,
			PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

	*(char *)p = 0;
	check_munmap(filename, lineno, p, MFD_DEF_SIZE);

	/* verify PROT_WRITE is allowed */
	p = check_mmap(filename, lineno, NULL, MFD_DEF_SIZE,
			PROT_WRITE, MAP_SHARED, fd, 0);

	*(char *)p = 0;
	check_munmap(filename, lineno, p, MFD_DEF_SIZE);

	/* verify PROT_READ with MAP_SHARED is allowed and a following
	 * mprotect(PROT_WRITE) allows writing
	 */
	p = check_mmap(filename, lineno, NULL, MFD_DEF_SIZE,
			PROT_READ, MAP_SHARED, fd, 0);

	check_mprotect(filename, lineno, p, MFD_DEF_SIZE,
			PROT_READ | PROT_WRITE);

	*(char *)p = 0;
	check_munmap(filename, lineno, p, MFD_DEF_SIZE);

	/* verify PUNCH_HOLE works */
	check_fallocate(filename, lineno, fd,
			FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0,
			MFD_DEF_SIZE);
}

void check_mfd_non_writeable(const char *filename, const int lineno,
				int fd)
{
	void *p;

	/* verify write() fails */
	TEST(write(fd, "data", 4));
	if (TEST_RETURN < 0) {
		if (TEST_ERRNO != EPERM) {
			tst_brk_(filename, lineno, TFAIL | TTERRNO,
				"write() didn't fail as expected");
		}
	} else {
		tst_brk_(filename, lineno, TFAIL,
			"write() succeeded unexpectedly");
	}
	tst_res_(filename, lineno, TPASS | TTERRNO, "write failed as expected");

	/* verify PROT_READ | PROT_WRITE is not allowed */
	check_mmap_fail(filename, lineno, NULL, MFD_DEF_SIZE,
			PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

	/* verify PROT_WRITE is not allowed */
	check_mmap_fail(filename, lineno, NULL, MFD_DEF_SIZE,
			PROT_WRITE, MAP_SHARED, fd, 0);

	/* Verify PROT_READ with MAP_SHARED with a following mprotect is not
	 * allowed. Note that for r/w the kernel already prevents the mmap.
	 */
	p = mmap(NULL, MFD_DEF_SIZE, PROT_READ, MAP_SHARED, fd, 0);
	if (p != MAP_FAILED) {
		if (mprotect(p, MFD_DEF_SIZE, PROT_READ | PROT_WRITE) >= 0) {
			tst_brk_(filename, lineno, TFAIL | TERRNO,
				"mmap()+mprotect() succeeded unexpectedly");
		}
	}

	/* verify PUNCH_HOLE fails */
	check_fallocate_fail(filename, lineno, fd,
			FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0,
			MFD_DEF_SIZE);
}

void check_mfd_shrinkable(const char *filename, const int lineno, int fd)
{
	int fd2;

	check_ftruncate(filename, lineno, fd, MFD_DEF_SIZE / 2);
	check_mfd_size(filename, lineno, fd, MFD_DEF_SIZE / 2);

	fd2 = check_mfd_open(filename, lineno, fd,
			O_RDWR | O_CREAT | O_TRUNC, 0600);
	safe_close(filename, lineno, NULL, fd2);

	check_mfd_size(filename, lineno, fd, 0);
}

void check_mfd_non_shrinkable(const char *filename, const int lineno, int fd)
{
	check_ftruncate_fail(filename, lineno, fd,  MFD_DEF_SIZE / 2);
	check_mfd_fail_open(filename, lineno, fd,
			O_RDWR | O_CREAT | O_TRUNC, 0600);
}

void check_mfd_growable(const char *filename, const int lineno, int fd)
{
	check_ftruncate(filename, lineno, fd, MFD_DEF_SIZE * 2);
	check_mfd_size(filename, lineno, fd, MFD_DEF_SIZE * 2);

	check_fallocate(filename, lineno, fd, 0, 0, MFD_DEF_SIZE * 4);
	check_mfd_size(filename, lineno, fd, MFD_DEF_SIZE * 4);
}

void check_mfd_non_growable(const char *filename, const int lineno, int fd)
{
	check_ftruncate_fail(filename, lineno, fd, MFD_DEF_SIZE * 2);
	check_fallocate_fail(filename, lineno, fd, 0, 0, MFD_DEF_SIZE * 4);
}

void check_mfd_growable_by_write(const char *filename, const int lineno, int fd)
{
	char buf[MFD_DEF_SIZE * 8];

	if (pwrite(fd, buf, sizeof(buf), 0) != sizeof(buf)) {
		tst_res_(filename, lineno, TFAIL | TERRNO,
			"pwrite(%d, %s, %ld, %d) failed",
			fd, buf, sizeof(buf), 0);

		return;
	}

	tst_res_(filename, lineno, TPASS, "pwrite(%d, %s, %ld, %d) succeeded",
		fd, buf, sizeof(buf), 0);

	check_mfd_size(filename, lineno, fd, MFD_DEF_SIZE * 8);
}

void check_mfd_non_growable_by_write(const char *filename, const int lineno,
					int fd)
{
	char buf[MFD_DEF_SIZE * 8];

	if (pwrite(fd, buf, sizeof(buf), 0) == sizeof(buf)) {
		tst_res_(filename, lineno, TFAIL,
			"pwrite(%d, %s, %ld, %d) didn't fail as expected",
			fd, buf, sizeof(buf), 0);

		return;
	}

	tst_res_(filename, lineno, TPASS, "pwrite(%d, %s, %ld, %d) succeeded",
		fd, buf, sizeof(buf), 0);
}
