275 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			C
		
	
	
	
			
		
		
	
	
			275 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			C
		
	
	
	
| // SPDX-License-Identifier: GPL-2.0-or-later
 | |
| /*
 | |
|  * Copyright (c) 2019 SUSE LLC <mdoucha@suse.cz>
 | |
|  */
 | |
| 
 | |
| /*\
 | |
|  * [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 <stdio.h>
 | |
| #include <stdlib.h>
 | |
| #include <string.h>
 | |
| #include <fcntl.h>
 | |
| #include <sys/ioctl.h>
 | |
| #include <linux/fs.h>
 | |
| #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, 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(1, 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);
 | |
| 
 | |
| 	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,
 | |
| 	.mount_device = 1,
 | |
| 	.dev_min_size = 512,
 | |
| 	.mntpoint = MNTPOINT,
 | |
| 	.all_filesystems = 1,
 | |
| 	.setup = setup,
 | |
| 	.cleanup = cleanup,
 | |
| 	.tags = (const struct tst_tag[]) {
 | |
| 		{"linux-git", "e093c4be760e"},
 | |
| 		{"linux-git", "6d4572a9d71d"},
 | |
| 		{}
 | |
| 	}
 | |
| };
 |