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