1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Copyright (c) 2019 SUSE LLC <mdoucha@suse.cz>
4 */
5
6/*\
7 * [Description]
8 *
9 * Tests misaligned fallocate()
10 *
11 * Test scenario:
12 *
13 * 1. write() several blocks worth of data
14 * 2. fallocate() some more space (not aligned to FS blocks)
15 * 3. try to write() into the allocated space
16 * 4. deallocate misaligned part of file range written in step 1
17 * 5. read() the deallocated range and check that it was zeroed
18 *
19 * Subtests:
20 *
21 * - fill filesystem between step 2 and 3
22 * - disable copy-on-write on test file
23 * - combinations of above subtests
24 */
25
26/*
27 * This is also regression test for:
28 * e093c4be760e ("xfs: Fix tail rounding in xfs_alloc_file_space()")
29 * 6d4572a9d71d ("Allow btrfs_truncate_block() to fallback to nocow for data
30 *              space reservation")
31 */
32
33#define _GNU_SOURCE
34
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38#include <fcntl.h>
39#include <sys/ioctl.h>
40#include <linux/fs.h>
41#include "tst_test.h"
42#include "lapi/fallocate.h"
43
44#define MNTPOINT "mntpoint"
45#define TEMPFILE MNTPOINT "/test_file"
46#define WRITE_BLOCKS 8
47#define FALLOCATE_BLOCKS 2
48#define DEALLOCATE_BLOCKS 3
49#define TESTED_FLAGS "fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)"
50
51const struct test_case {
52	int no_cow, fill_fs;
53} testcase_list[] = {
54	{1, 0},
55	{1, 1},
56	{0, 0},
57	{0, 1}
58};
59
60static int cow_support;
61static char *wbuf, *rbuf;
62static blksize_t blocksize;
63static long wbuf_size, rbuf_size, block_offset;
64
65static int toggle_cow(int fd, int enable)
66{
67	int ret, attr;
68
69	ret = ioctl(fd, FS_IOC_GETFLAGS, &attr);
70
71	if (ret)
72		return ret;
73
74	if (enable)
75		attr &= ~FS_NOCOW_FL;
76	else
77		attr |= FS_NOCOW_FL;
78
79	return ioctl(fd, FS_IOC_SETFLAGS, &attr);
80}
81
82static void setup(void)
83{
84	unsigned char ch;
85	long i;
86	int fd;
87	struct stat statbuf;
88
89	fd = SAFE_OPEN(TEMPFILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
90
91	/*
92	 * Set FS_NOCOW_FL flag on the temp file. Non-CoW filesystems will
93	 * return error.
94	 */
95	TEST(toggle_cow(fd, 0));
96	SAFE_FSTAT(fd, &statbuf);
97	blocksize = statbuf.st_blksize;
98	block_offset = MIN(blocksize / 2, (blksize_t)512);
99	wbuf_size = MAX(WRITE_BLOCKS, FALLOCATE_BLOCKS) * blocksize;
100	rbuf_size = (DEALLOCATE_BLOCKS + 1) * blocksize;
101	SAFE_CLOSE(fd);
102	SAFE_UNLINK(TEMPFILE);
103
104	if (blocksize < 2)
105		tst_brk(TCONF, "Block size %ld too small for test", blocksize);
106
107	if (!TST_RET) {
108		cow_support = 1;
109	} else {
110		switch (TST_ERR) {
111		case ENOTSUP:
112		case ENOTTY:
113		case EINVAL:
114		case ENOSYS:
115			cow_support = 0;
116			break;
117
118		default:
119			tst_brk(TBROK|TTERRNO,
120				"Error checking copy-on-write support");
121			break;
122		}
123	}
124
125	tst_res(TINFO, "Copy-on-write is%s supported",
126		cow_support ? "" : " not");
127	wbuf = SAFE_MALLOC(wbuf_size);
128	rbuf = SAFE_MALLOC(rbuf_size);
129
130	/* Fill the buffer with known values */
131	for (i = 0, ch = 1; i < wbuf_size; i++, ch++)
132		wbuf[i] = ch;
133}
134
135static int check_result(const struct test_case *tc, const char *func, long exp)
136{
137	if (tc->fill_fs && !tc->no_cow && TST_RET < 0) {
138		if (TST_RET != -1) {
139			tst_res(TFAIL, "%s returned unexpected value %ld",
140				func, TST_RET);
141			return 0;
142		}
143
144		if (TST_ERR != ENOSPC) {
145			tst_res(TFAIL | TTERRNO, "%s should fail with ENOSPC",
146				func);
147			return 0;
148		}
149
150		tst_res(TPASS | TTERRNO, "%s on full FS with CoW", func);
151		return 1;
152	}
153
154	if (TST_RET < 0) {
155		tst_res(TFAIL | TTERRNO, "%s failed unexpectedly", func);
156		return 0;
157	}
158
159	if (TST_RET != exp) {
160		tst_res(TFAIL,
161			"Unexpected return value from %s: %ld (expected %ld)",
162			func, TST_RET, exp);
163		return 0;
164	}
165
166	tst_res(TPASS, "%s successful", func);
167	return 1;
168}
169
170static void run(unsigned int n)
171{
172	int fd, i, err;
173	long offset, size;
174	const struct test_case *tc = testcase_list + n;
175
176	tst_res(TINFO, "Case %u. Fill FS: %s; Use copy on write: %s", n+1,
177		tc->fill_fs ? "yes" : "no", tc->no_cow ? "no" : "yes");
178	fd = SAFE_OPEN(TEMPFILE, O_RDWR | O_CREAT | O_TRUNC, 0644);
179
180	if (cow_support)
181		toggle_cow(fd, !tc->no_cow);
182	else if (!tc->no_cow)
183		tst_brk(TCONF, "File system does not support copy-on-write");
184
185	/* Prepare test data for deallocation test */
186	size = WRITE_BLOCKS * blocksize;
187	SAFE_WRITE(SAFE_WRITE_ALL, fd, wbuf, size);
188
189	/* Allocation test */
190	offset = size + block_offset;
191	size = FALLOCATE_BLOCKS * blocksize;
192	TEST(fallocate(fd, 0, offset, size));
193
194	if (TST_RET) {
195		SAFE_CLOSE(fd);
196
197		if (TST_ERR == ENOTSUP)
198			tst_brk(TCONF | TTERRNO, "fallocate() not supported");
199
200		tst_brk(TBROK | TTERRNO, "fallocate(fd, 0, %ld, %ld)", offset,
201			size);
202	}
203
204	if (tc->fill_fs)
205		tst_fill_fs(MNTPOINT, 1, TST_FILL_RANDOM);
206
207	SAFE_LSEEK(fd, offset, SEEK_SET);
208	TEST(write(fd, wbuf, size));
209	if (check_result(tc, "write()", size))
210		tst_res(TPASS, "Misaligned allocation works as expected");
211
212	/* Deallocation test */
213	size = DEALLOCATE_BLOCKS * blocksize;
214	offset = block_offset;
215	TEST(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, offset,
216		size));
217
218	if (TST_RET == -1 && TST_ERR == ENOTSUP) {
219		tst_res(TCONF | TTERRNO, TESTED_FLAGS);
220		goto end;
221	}
222
223	if (!check_result(tc, TESTED_FLAGS, 0) || TST_RET)
224		goto end;
225
226	/* Validate that fallocate() cleared the correct file range */
227	SAFE_LSEEK(fd, 0, SEEK_SET);
228	SAFE_READ(1, fd, rbuf, rbuf_size);
229
230	for (err = 0, i = offset; i < offset + size; i++) {
231		if (rbuf[i]) {
232			err = 1;
233			break;
234		}
235	}
236
237	err = err || memcmp(rbuf, wbuf, offset);
238	offset += size;
239	size = rbuf_size - offset;
240	err = err || memcmp(rbuf + offset, wbuf + offset, size);
241
242	if (err)
243		tst_res(TFAIL, TESTED_FLAGS
244			" did not clear the correct file range.");
245	else
246		tst_res(TPASS, TESTED_FLAGS " cleared the correct file range");
247
248end:
249	SAFE_CLOSE(fd);
250	tst_purge_dir(MNTPOINT);
251}
252
253static void cleanup(void)
254{
255	free(wbuf);
256	free(rbuf);
257}
258
259static struct tst_test test = {
260	.test = run,
261	.tcnt = ARRAY_SIZE(testcase_list),
262	.needs_root = 1,
263	.mount_device = 1,
264	.mntpoint = MNTPOINT,
265	.all_filesystems = 1,
266	.setup = setup,
267	.cleanup = cleanup,
268	.tags = (const struct tst_tag[]) {
269		{"linux-git", "e093c4be760e"},
270		{"linux-git", "6d4572a9d71d"},
271		{}
272	}
273};
274