// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (c) 2019 SUSE LLC */ /*\ * [Description] * * Tests misaligned fallocate() * * Test scenario: * * 1. write() several blocks worth of data * 2. fallocate() some more space (not aligned to FS blocks) * 3. try to write() into the allocated space * 4. deallocate misaligned part of file range written in step 1 * 5. read() the deallocated range and check that it was zeroed * * Subtests: * * - fill filesystem between step 2 and 3 * - disable copy-on-write on test file * - combinations of above subtests */ /* * This is also regression test for: * e093c4be760e ("xfs: Fix tail rounding in xfs_alloc_file_space()") * 6d4572a9d71d ("Allow btrfs_truncate_block() to fallback to nocow for data * space reservation") */ #define _GNU_SOURCE #include #include #include #include #include #include #include "tst_test.h" #include "lapi/fallocate.h" #define MNTPOINT "mntpoint" #define TEMPFILE MNTPOINT "/test_file" #define WRITE_BLOCKS 8 #define FALLOCATE_BLOCKS 2 #define DEALLOCATE_BLOCKS 3 #define TESTED_FLAGS "fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)" const struct test_case { int no_cow, fill_fs; } testcase_list[] = { {1, 0}, {1, 1}, {0, 0}, {0, 1} }; static int cow_support; static char *wbuf, *rbuf; static blksize_t blocksize; static long wbuf_size, rbuf_size, block_offset; static int toggle_cow(int fd, int enable) { int ret, attr; ret = ioctl(fd, FS_IOC_GETFLAGS, &attr); if (ret) return ret; if (enable) attr &= ~FS_NOCOW_FL; else attr |= FS_NOCOW_FL; return ioctl(fd, FS_IOC_SETFLAGS, &attr); } static void setup(void) { unsigned char ch; long i; int fd; struct stat statbuf; fd = SAFE_OPEN(TEMPFILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); /* * Set FS_NOCOW_FL flag on the temp file. Non-CoW filesystems will * return error. */ TEST(toggle_cow(fd, 0)); SAFE_FSTAT(fd, &statbuf); blocksize = statbuf.st_blksize; block_offset = MIN(blocksize / 2, (blksize_t)512); wbuf_size = MAX(WRITE_BLOCKS, FALLOCATE_BLOCKS) * blocksize; rbuf_size = (DEALLOCATE_BLOCKS + 1) * blocksize; SAFE_CLOSE(fd); SAFE_UNLINK(TEMPFILE); if (blocksize < 2) tst_brk(TCONF, "Block size %ld too small for test", blocksize); if (!TST_RET) { cow_support = 1; } else { switch (TST_ERR) { case ENOTSUP: case ENOTTY: case EINVAL: case ENOSYS: cow_support = 0; break; default: tst_brk(TBROK|TTERRNO, "Error checking copy-on-write support"); break; } } tst_res(TINFO, "Copy-on-write is%s supported", cow_support ? "" : " not"); wbuf = SAFE_MALLOC(wbuf_size); rbuf = SAFE_MALLOC(rbuf_size); /* Fill the buffer with known values */ for (i = 0, ch = 1; i < wbuf_size; i++, ch++) wbuf[i] = ch; } static int check_result(const struct test_case *tc, const char *func, long exp) { if (tc->fill_fs && !tc->no_cow && TST_RET < 0) { if (TST_RET != -1) { tst_res(TFAIL, "%s returned unexpected value %ld", func, TST_RET); return 0; } if (TST_ERR != ENOSPC) { tst_res(TFAIL | TTERRNO, "%s should fail with ENOSPC", func); return 0; } tst_res(TPASS | TTERRNO, "%s on full FS with CoW", func); return 1; } if (TST_RET < 0) { tst_res(TFAIL | TTERRNO, "%s failed unexpectedly", func); return 0; } if (TST_RET != exp) { tst_res(TFAIL, "Unexpected return value from %s: %ld (expected %ld)", func, TST_RET, exp); return 0; } tst_res(TPASS, "%s successful", func); return 1; } static void run(unsigned int n) { int fd, i, err; long offset, size; const struct test_case *tc = testcase_list + n; tst_res(TINFO, "Case %u. Fill FS: %s; Use copy on write: %s", n+1, tc->fill_fs ? "yes" : "no", tc->no_cow ? "no" : "yes"); fd = SAFE_OPEN(TEMPFILE, O_RDWR | O_CREAT | O_TRUNC, 0644); if (cow_support) toggle_cow(fd, !tc->no_cow); else if (!tc->no_cow) tst_brk(TCONF, "File system does not support copy-on-write"); /* Prepare test data for deallocation test */ size = WRITE_BLOCKS * blocksize; SAFE_WRITE(SAFE_WRITE_ALL, fd, wbuf, size); /* Allocation test */ offset = size + block_offset; size = FALLOCATE_BLOCKS * blocksize; TEST(fallocate(fd, 0, offset, size)); if (TST_RET) { SAFE_CLOSE(fd); if (TST_ERR == ENOTSUP) tst_brk(TCONF | TTERRNO, "fallocate() not supported"); tst_brk(TBROK | TTERRNO, "fallocate(fd, 0, %ld, %ld)", offset, size); } if (tc->fill_fs) tst_fill_fs(MNTPOINT, 1, TST_FILL_RANDOM); SAFE_LSEEK(fd, offset, SEEK_SET); TEST(write(fd, wbuf, size)); if (check_result(tc, "write()", size)) tst_res(TPASS, "Misaligned allocation works as expected"); /* Deallocation test */ size = DEALLOCATE_BLOCKS * blocksize; offset = block_offset; TEST(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset, size)); if (TST_RET == -1 && TST_ERR == ENOTSUP) { tst_res(TCONF | TTERRNO, TESTED_FLAGS); goto end; } if (!check_result(tc, TESTED_FLAGS, 0) || TST_RET) goto end; /* Validate that fallocate() cleared the correct file range */ SAFE_LSEEK(fd, 0, SEEK_SET); SAFE_READ(1, fd, rbuf, rbuf_size); for (err = 0, i = offset; i < offset + size; i++) { if (rbuf[i]) { err = 1; break; } } err = err || memcmp(rbuf, wbuf, offset); offset += size; size = rbuf_size - offset; err = err || memcmp(rbuf + offset, wbuf + offset, size); if (err) tst_res(TFAIL, TESTED_FLAGS " did not clear the correct file range."); else tst_res(TPASS, TESTED_FLAGS " cleared the correct file range"); end: SAFE_CLOSE(fd); tst_purge_dir(MNTPOINT); } static void cleanup(void) { free(wbuf); free(rbuf); } static struct tst_test test = { .test = run, .tcnt = ARRAY_SIZE(testcase_list), .needs_root = 1, .dev_min_size = 1024, .max_runtime = 120, .mount_device = 1, .mntpoint = MNTPOINT, .all_filesystems = 1, .setup = setup, .cleanup = cleanup, .tags = (const struct tst_tag[]) { {"linux-git", "e093c4be760e"}, {"linux-git", "6d4572a9d71d"}, {} } };