/* * * 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 #include #include #include #include #include #include #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, };