// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *
 *   Copyright (c) Linux Test Project, 2016
 */

/*\
 * [Description]
 *
 * Verify writev() behaviour with partially valid iovec list.
 * Kernel <4.8 used to shorten write up to first bad invalid
 * iovec. Starting with 4.8, a writev with short data (under
 * page size) is likely to get shorten to 0 bytes and return
 * EFAULT.
 *
 * This test doesn't make assumptions how much will write get
 * shortened. It only tests that file content/offset after
 * syscall corresponds to return value of writev().
 *
 * See: [RFC] writev() semantics with invalid iovec in the middle
 * https://marc.info/?l=linux-kernel&m=147388891614289&w=2.
 *
 * This is also regression test for kernel commits:
 *
 * * 20c64ec83a9f ("iomap: fix a regression for partial write errors")
 * * 3ac974796e5d ("iomap: fix short copy in iomap_write_iter()")
 */

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include "tst_test.h"

#define TESTFILE "testfile"
#define CHUNK 64
#define BUFSIZE (CHUNK * 4)

static void *bad_addr;

static void test_partially_valid_iovec(int initial_file_offset)
{
	int i, fd;
	unsigned char buffer[BUFSIZE], fpattern[BUFSIZE], tmp[BUFSIZE];
	long off_after;
	struct iovec wr_iovec[] = {
		{ buffer, CHUNK },
		{ bad_addr, CHUNK },
		{ buffer + CHUNK, CHUNK },
		{ buffer + CHUNK * 2, CHUNK },
	};

	tst_res(TINFO, "starting test with initial file offset: %d ",
		initial_file_offset);

	for (i = 0; i < BUFSIZE; i++)
		buffer[i] = i % (CHUNK - 1);

	memset(fpattern, 0xff, BUFSIZE);
	tst_fill_file(TESTFILE, 0xff, CHUNK, BUFSIZE / CHUNK);

	fd = SAFE_OPEN(TESTFILE, O_RDWR, 0644);
	SAFE_LSEEK(fd, initial_file_offset, SEEK_SET);
	TEST(writev(fd, wr_iovec, ARRAY_SIZE(wr_iovec)));
	off_after = (long) SAFE_LSEEK(fd, 0, SEEK_CUR);

	/* bad errno */
	if (TST_RET == -1 && TST_ERR != EFAULT) {
		tst_res(TFAIL | TTERRNO, "unexpected errno");
		SAFE_CLOSE(fd);
		return;
	}

	/* nothing has been written */
	if (TST_RET == -1 && TST_ERR == EFAULT) {
		tst_res(TINFO, "got EFAULT");
		/* initial file content remains untouched */
		SAFE_LSEEK(fd, 0, SEEK_SET);
		SAFE_READ(1, fd, tmp, BUFSIZE);
		if (memcmp(tmp, fpattern, BUFSIZE))
			tst_res(TFAIL, "file was written to");
		else
			tst_res(TPASS, "file stayed untouched");

		/* offset hasn't changed */
		if (off_after == initial_file_offset)
			tst_res(TPASS, "offset stayed unchanged");
		else
			tst_res(TFAIL, "offset changed to %ld",
				off_after);

		SAFE_CLOSE(fd);
		return;
	}

	/* writev() wrote more bytes than bytes preceding invalid iovec */
	tst_res(TINFO, "writev() has written %ld bytes", TST_RET);
	if (TST_RET > (long) wr_iovec[0].iov_len) {
		tst_res(TFAIL, "writev wrote more than expected");
		SAFE_CLOSE(fd);
		return;
	}

	/* file content matches written bytes */
	SAFE_LSEEK(fd, initial_file_offset, SEEK_SET);
	SAFE_READ(1, fd, tmp, TST_RET);
	if (memcmp(tmp, wr_iovec[0].iov_base, TST_RET) == 0) {
		tst_res(TPASS, "file has expected content");
	} else {
		tst_res(TFAIL, "file has unexpected content");
		tst_res_hexd(TFAIL, wr_iovec[0].iov_base, TST_RET,
				"expected:");
		tst_res_hexd(TFAIL, tmp, TST_RET,
				"actual file content:");
	}

	/* file offset has been updated according to written bytes */
	if (off_after == initial_file_offset + TST_RET)
		tst_res(TPASS, "offset at %ld as expected", off_after);
	else
		tst_res(TFAIL, "offset unexpected %ld", off_after);

	SAFE_CLOSE(fd);
}

static void test_writev(void)
{
	test_partially_valid_iovec(0);
	test_partially_valid_iovec(CHUNK + 1);
	test_partially_valid_iovec(getpagesize());
	test_partially_valid_iovec(getpagesize() + 1);
}

static void setup(void)
{
	bad_addr = SAFE_MMAP(0, 1, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS,
			0, 0);
}

static struct tst_test test = {
	.needs_tmpdir = 1,
	.setup = setup,
	.test_all = test_writev,
	.tags = (const struct tst_tag[]) {
		{"linux-git", "20c64ec83a9f"},
		{"linux-git", "3ac974796e5d"},
		{},
	}
};
