xref: /freebsd/tests/sys/fs/fusefs/last_local_modify.cc (revision e1e636193db45630c7881246d25902e57c43d24e)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2021 Alan Somers
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  */
27 
28 extern "C" {
29 #include <sys/param.h>
30 #include <sys/mount.h>
31 #include <sys/stat.h>
32 
33 #include <fcntl.h>
34 #include <pthread.h>
35 #include <semaphore.h>
36 }
37 
38 #include "mockfs.hh"
39 #include "utils.hh"
40 
41 using namespace testing;
42 
43 /*
44  * "Last Local Modify" bugs
45  *
46  * This file tests a class of race conditions caused by one thread fetching a
47  * file's size with FUSE_LOOKUP while another thread simultaneously modifies it
48  * with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar.  It's
49  * possible for the second thread to start later yet finish first. If that
50  * happens, the first thread must not override the size set by the second
51  * thread.
52  *
53  * FUSE_GETATTR is not vulnerable to the same race, because it is always called
54  * with the vnode lock held.
55  *
56  * A few other operations like FUSE_LINK can also trigger the same race but
57  * with the file's ctime instead of size.  However, the consequences of an
58  * incorrect ctime are much less disastrous than an incorrect size, so fusefs
59  * does not attempt to prevent such races.
60  */
61 
62 enum Mutator {
63 	VOP_ALLOCATE,
64 	VOP_COPY_FILE_RANGE,
65 	VOP_SETATTR,
66 	VOP_WRITE,
67 };
68 
69 /*
70  * Translate a poll method's string representation to the enum value.
71  * Using strings with ::testing::Values gives better output with
72  * --gtest_list_tests
73  */
74 enum Mutator writer_from_str(const char* s) {
75 	if (0 == strcmp("VOP_ALLOCATE", s))
76 		return VOP_ALLOCATE;
77 	else if (0 == strcmp("VOP_COPY_FILE_RANGE", s))
78 		return VOP_COPY_FILE_RANGE;
79 	else if (0 == strcmp("VOP_SETATTR", s))
80 		return VOP_SETATTR;
81 	else
82 		return VOP_WRITE;
83 }
84 
85 uint32_t fuse_op_from_mutator(enum Mutator mutator) {
86 	switch(mutator) {
87 	case VOP_ALLOCATE:
88 		return(FUSE_FALLOCATE);
89 	case VOP_COPY_FILE_RANGE:
90 		return(FUSE_COPY_FILE_RANGE);
91 	case VOP_SETATTR:
92 		return(FUSE_SETATTR);
93 	case VOP_WRITE:
94 		return(FUSE_WRITE);
95 	}
96 }
97 
98 class LastLocalModify: public FuseTest, public WithParamInterface<const char*> {
99 public:
100 virtual void SetUp() {
101 	m_init_flags = FUSE_EXPORT_SUPPORT;
102 
103 	FuseTest::SetUp();
104 }
105 };
106 
107 static void* allocate_th(void* arg) {
108 	int fd;
109 	ssize_t r;
110 	sem_t *sem = (sem_t*) arg;
111 
112 	if (sem)
113 		sem_wait(sem);
114 
115 	fd = open("mountpoint/some_file.txt", O_RDWR);
116 	if (fd < 0)
117 		return (void*)(intptr_t)errno;
118 
119 	r = posix_fallocate(fd, 0, 15);
120 	LastLocalModify::leak(fd);
121 	if (r >= 0)
122 		return 0;
123 	else
124 		return (void*)(intptr_t)errno;
125 }
126 
127 static void* copy_file_range_th(void* arg) {
128 	ssize_t r;
129 	int fd;
130 	sem_t *sem = (sem_t*) arg;
131 	off_t off_in = 0;
132 	off_t off_out = 10;
133 	ssize_t len = 5;
134 
135 	if (sem)
136 		sem_wait(sem);
137 	fd = open("mountpoint/some_file.txt", O_RDWR);
138 	if (fd < 0)
139 		return (void*)(intptr_t)errno;
140 
141 	r = copy_file_range(fd, &off_in, fd, &off_out, len, 0);
142 	if (r >= 0) {
143 		LastLocalModify::leak(fd);
144 		return 0;
145 	} else
146 		return (void*)(intptr_t)errno;
147 }
148 
149 static void* setattr_th(void* arg) {
150 	int fd;
151 	ssize_t r;
152 	sem_t *sem = (sem_t*) arg;
153 
154 	if (sem)
155 		sem_wait(sem);
156 
157 	fd = open("mountpoint/some_file.txt", O_RDWR);
158 	if (fd < 0)
159 		return (void*)(intptr_t)errno;
160 
161 	r = ftruncate(fd, 15);
162 	LastLocalModify::leak(fd);
163 	if (r >= 0)
164 		return 0;
165 	else
166 		return (void*)(intptr_t)errno;
167 }
168 
169 static void* write_th(void* arg) {
170 	ssize_t r;
171 	int fd;
172 	sem_t *sem = (sem_t*) arg;
173 	const char BUF[] = "abcdefghijklmn";
174 
175 	if (sem)
176 		sem_wait(sem);
177 	fd = open("mountpoint/some_file.txt", O_RDWR);
178 	if (fd < 0)
179 		return (void*)(intptr_t)errno;
180 
181 	r = write(fd, BUF, sizeof(BUF));
182 	if (r >= 0) {
183 		LastLocalModify::leak(fd);
184 		return 0;
185 	} else
186 		return (void*)(intptr_t)errno;
187 }
188 
189 /*
190  * VOP_LOOKUP should discard attributes returned by the server if they were
191  * modified by another VOP while the VOP_LOOKUP was in progress.
192  *
193  * Sequence of operations:
194  * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock
195  *   exclusively.
196  * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the
197  *   server.  The server replies with the old file length.  Thread 2 blocks
198  *   waiting for the vnode lock.
199  * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the
200  *   file's size and updates the attribute cache.  Then it releases the vnode
201  *   lock.
202  * * Thread 2 acquires the vnode lock.  At this point it must not add the
203  *   now-stale file size to the attribute cache.
204  *
205  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
206  */
207 TEST_P(LastLocalModify, lookup)
208 {
209 	const char FULLPATH[] = "mountpoint/some_file.txt";
210 	const char RELPATH[] = "some_file.txt";
211 	Sequence seq;
212 	uint64_t ino = 3;
213 	uint64_t mutator_unique;
214 	const uint64_t oldsize = 10;
215 	const uint64_t newsize = 15;
216 	pthread_t th0;
217 	void *thr0_value;
218 	struct stat sb;
219 	static sem_t sem;
220 	Mutator mutator;
221 	uint32_t mutator_op;
222 	size_t mutator_size;
223 
224 	mutator = writer_from_str(GetParam());
225 	mutator_op = fuse_op_from_mutator(mutator);
226 
227 	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
228 
229 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
230 	.InSequence(seq)
231 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
232 		/* Called by the mutator, caches attributes but not entries */
233 		SET_OUT_HEADER_LEN(out, entry);
234 		out.body.entry.nodeid = ino;
235 		out.body.entry.attr.size = oldsize;
236 		out.body.entry.nodeid = ino;
237 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
238 		out.body.entry.attr.ino = ino;
239 		out.body.entry.attr.mode = S_IFREG | 0644;
240 	})));
241 	expect_open(ino, 0, 1);
242 	EXPECT_CALL(*m_mock, process(
243 		ResultOf([=](auto in) {
244 			return (in.header.opcode == mutator_op &&
245 				in.header.nodeid == ino);
246 		}, Eq(true)),
247 		_)
248 	).InSequence(seq)
249 	.WillOnce(Invoke([&](auto in, auto &out __unused) {
250 		/*
251 		 * The mutator changes the file size, but in order to simulate
252 		 * a race, don't reply.  Instead, just save the unique for
253 		 * later.
254 		 */
255 		mutator_unique = in.header.unique;
256 		switch(mutator) {
257 		case VOP_WRITE:
258 			mutator_size = in.body.write.size;
259 			break;
260 		case VOP_COPY_FILE_RANGE:
261 			mutator_size = in.body.copy_file_range.len;
262 			break;
263 		default:
264 			break;
265 		}
266 		/* Allow the lookup thread to proceed */
267 		sem_post(&sem);
268 	}));
269 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
270 	.InSequence(seq)
271 	.WillOnce(Invoke([&](auto in __unused, auto& out) {
272 		std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
273 		std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
274 
275 		/* First complete the lookup request, returning the old size */
276 		out0->header.unique = in.header.unique;
277 		SET_OUT_HEADER_LEN(*out0, entry);
278 		out0->body.entry.attr.mode = S_IFREG | 0644;
279 		out0->body.entry.nodeid = ino;
280 		out0->body.entry.entry_valid = UINT64_MAX;
281 		out0->body.entry.attr_valid = UINT64_MAX;
282 		out0->body.entry.attr.size = oldsize;
283 		out.push_back(std::move(out0));
284 
285 		/* Then, respond to the mutator request */
286 		out1->header.unique = mutator_unique;
287 		switch(mutator) {
288 		case VOP_ALLOCATE:
289 			out1->header.error = 0;
290 			out1->header.len = sizeof(out1->header);
291 			break;
292 		case VOP_COPY_FILE_RANGE:
293 			SET_OUT_HEADER_LEN(*out1, write);
294 			out1->body.write.size = mutator_size;
295 			break;
296 		case VOP_SETATTR:
297 			SET_OUT_HEADER_LEN(*out1, attr);
298 			out1->body.attr.attr.ino = ino;
299 			out1->body.attr.attr.mode = S_IFREG | 0644;
300 			out1->body.attr.attr.size = newsize;	// Changed size
301 			out1->body.attr.attr_valid = UINT64_MAX;
302 			break;
303 		case VOP_WRITE:
304 			SET_OUT_HEADER_LEN(*out1, write);
305 			out1->body.write.size = mutator_size;
306 			break;
307 		}
308 		out.push_back(std::move(out1));
309 	}));
310 
311 	/* Start the mutator thread */
312 	switch(mutator) {
313 	case VOP_ALLOCATE:
314 		ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
315 			NULL)) << strerror(errno);
316 		break;
317 	case VOP_COPY_FILE_RANGE:
318 		ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
319 			NULL)) << strerror(errno);
320 		break;
321 	case VOP_SETATTR:
322 		ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))
323 			<< strerror(errno);
324 		break;
325 	case VOP_WRITE:
326 		ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))
327 			<< strerror(errno);
328 		break;
329 	}
330 
331 
332 	/* Wait for FUSE_SETATTR to be sent */
333 	sem_wait(&sem);
334 
335 	/* Lookup again, which will race with setattr */
336 	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
337 	ASSERT_EQ((off_t)newsize, sb.st_size);
338 
339 	/* ftruncate should've completed without error */
340 	pthread_join(th0, &thr0_value);
341 	EXPECT_EQ(0, (intptr_t)thr0_value);
342 }
343 
344 /*
345  * VFS_VGET should discard attributes returned by the server if they were
346  * modified by another VOP while the VFS_VGET was in progress.
347  *
348  * Sequence of operations:
349  * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP
350  * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock
351  *   exclusively and issues a FUSE op like FUSE_SETATTR.
352  * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks
353  *   waiting for the vnode lock.
354  * * Thread 2's FUSE op returns, and that thread sets the file's new size
355  *   in the attribute cache.  Finally it releases the vnode lock.
356  * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size
357  *   with the old value.
358  *
359  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
360  */
361 TEST_P(LastLocalModify, vfs_vget)
362 {
363 	const char FULLPATH[] = "mountpoint/some_file.txt";
364 	const char RELPATH[] = "some_file.txt";
365 	Sequence seq;
366 	uint64_t ino = 3;
367 	uint64_t lookup_unique;
368 	const uint64_t oldsize = 10;
369 	const uint64_t newsize = 15;
370 	pthread_t th0;
371 	void *thr0_value;
372 	struct stat sb;
373 	static sem_t sem;
374 	fhandle_t fhp;
375 	Mutator mutator;
376 	uint32_t mutator_op;
377 
378 	if (geteuid() != 0)
379 		GTEST_SKIP() << "This test requires a privileged user";
380 
381 	mutator = writer_from_str(GetParam());
382 	mutator_op = fuse_op_from_mutator(mutator);
383 
384 	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
385 
386 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
387 	.Times(1)
388 	.InSequence(seq)
389 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
390 	{
391 		/* Called by getfh, caches attributes but not entries */
392 		SET_OUT_HEADER_LEN(out, entry);
393 		out.body.entry.nodeid = ino;
394 		out.body.entry.attr.size = oldsize;
395 		out.body.entry.nodeid = ino;
396 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
397 		out.body.entry.attr.ino = ino;
398 		out.body.entry.attr.mode = S_IFREG | 0644;
399 	})));
400 	EXPECT_LOOKUP(ino, ".")
401 	.InSequence(seq)
402 	.WillOnce(Invoke([&](auto in, auto &out __unused) {
403 		/* Called by fhstat.  Block to simulate a race */
404 		lookup_unique = in.header.unique;
405 		sem_post(&sem);
406 	}));
407 
408 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
409 	.Times(1)
410 	.InSequence(seq)
411 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
412 	{
413 		/* Called by VOP_SETATTR, caches attributes but not entries */
414 		SET_OUT_HEADER_LEN(out, entry);
415 		out.body.entry.nodeid = ino;
416 		out.body.entry.attr.size = oldsize;
417 		out.body.entry.nodeid = ino;
418 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
419 		out.body.entry.attr.ino = ino;
420 		out.body.entry.attr.mode = S_IFREG | 0644;
421 	})));
422 
423 	/* Called by the mutator thread */
424 	expect_open(ino, 0, 1);
425 
426 	EXPECT_CALL(*m_mock, process(
427 		ResultOf([=](auto in) {
428 			return (in.header.opcode == mutator_op &&
429 				in.header.nodeid == ino);
430 		}, Eq(true)),
431 		_)
432 	).InSequence(seq)
433 	.WillOnce(Invoke([&](auto in __unused, auto& out) {
434 		std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
435 		std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
436 
437 		/* First complete the lookup request, returning the old size */
438 		out0->header.unique = lookup_unique;
439 		SET_OUT_HEADER_LEN(*out0, entry);
440 		out0->body.entry.attr.mode = S_IFREG | 0644;
441 		out0->body.entry.nodeid = ino;
442 		out0->body.entry.entry_valid = UINT64_MAX;
443 		out0->body.entry.attr_valid = UINT64_MAX;
444 		out0->body.entry.attr.size = oldsize;
445 		out.push_back(std::move(out0));
446 
447 		/* Then, respond to the mutator request */
448 		out1->header.unique = in.header.unique;
449 		switch(mutator) {
450 		case VOP_ALLOCATE:
451 			out1->header.error = 0;
452 			out1->header.len = sizeof(out1->header);
453 			break;
454 		case VOP_COPY_FILE_RANGE:
455 			SET_OUT_HEADER_LEN(*out1, write);
456 			out1->body.write.size = in.body.copy_file_range.len;
457 			break;
458 		case VOP_SETATTR:
459 			SET_OUT_HEADER_LEN(*out1, attr);
460 			out1->body.attr.attr.ino = ino;
461 			out1->body.attr.attr.mode = S_IFREG | 0644;
462 			out1->body.attr.attr.size = newsize;	// Changed size
463 			out1->body.attr.attr_valid = UINT64_MAX;
464 			break;
465 		case VOP_WRITE:
466 			SET_OUT_HEADER_LEN(*out1, write);
467 			out1->body.write.size = in.body.write.size;
468 			break;
469 		}
470 		out.push_back(std::move(out1));
471 	}));
472 
473 	/* First get a file handle */
474 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
475 
476 	/* Start the mutator thread */
477 	switch(mutator) {
478 	case VOP_ALLOCATE:
479 		ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
480 			(void*)&sem)) << strerror(errno);
481 		break;
482 	case VOP_COPY_FILE_RANGE:
483 		ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
484 			(void*)&sem)) << strerror(errno);
485 		break;
486 	case VOP_SETATTR:
487 		ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,
488 			(void*)&sem)) << strerror(errno);
489 		break;
490 	case VOP_WRITE:
491 		ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))
492 			<< strerror(errno);
493 		break;
494 	}
495 
496 	/* Lookup again, which will race with setattr */
497 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
498 
499 	ASSERT_EQ((off_t)newsize, sb.st_size);
500 
501 	/* mutator should've completed without error */
502 	pthread_join(th0, &thr0_value);
503 	EXPECT_EQ(0, (intptr_t)thr0_value);
504 }
505 
506 
507 INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify,
508 	Values(
509 		"VOP_ALLOCATE",
510 		"VOP_COPY_FILE_RANGE",
511 		"VOP_SETATTR",
512 		"VOP_WRITE"
513 	)
514 );
515