/*
*
* Copyright (c) Linux Test Project, 2016
*
* 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 will 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.
*/
/*
* Test 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
*/
#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 (TEST_RETURN == -1 && TEST_ERRNO != EFAULT) {
tst_res(TFAIL | TTERRNO, "unexpected errno");
SAFE_CLOSE(fd);
return;
}
/* nothing has been written */
if (TEST_RETURN == -1 && TEST_ERRNO == 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", TEST_RETURN);
if (TEST_RETURN > (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, TEST_RETURN);
if (memcmp(tmp, wr_iovec[0].iov_base, TEST_RETURN) == 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, TEST_RETURN,
"expected:");
tst_res_hexd(TFAIL, tmp, TEST_RETURN,
"actual file content:");
}
/* file offset has been updated according to written bytes */
if (off_after == initial_file_offset + TEST_RETURN)
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,
};