xref: /freebsd/tests/sys/fs/fusefs/last_local_modify.cc (revision d1eaa52d10f9b85e5f6358e1a280899b9d55dd07)
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  */
writer_from_str(const char * s)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 
fuse_op_from_mutator(enum Mutator mutator)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:
SetUp()100 virtual void SetUp() {
101 	m_init_flags = FUSE_EXPORT_SUPPORT;
102 
103 	FuseTest::SetUp();
104 }
105 };
106 
allocate_th(void * arg)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 
copy_file_range_th(void * arg)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 
setattr_th(void * arg)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 
write_th(void * arg)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 	/*
178 	 * Open the file in direct mode.
179 	 * The race condition affects both direct and non-direct writes, and
180 	 * they have separate code paths.  However, in the non-direct case, the
181 	 * kernel updates last_local_modify _before_ sending FUSE_WRITE to the
182 	 * server.  So the technique that this test program uses to invoke the
183 	 * race cannot work.  Therefore, test with O_DIRECT only.
184 	 */
185 	fd = open("mountpoint/some_file.txt", O_RDWR | O_DIRECT);
186 	if (fd < 0)
187 		return (void*)(intptr_t)errno;
188 
189 	r = write(fd, BUF, sizeof(BUF));
190 	if (r >= 0) {
191 		LastLocalModify::leak(fd);
192 		return 0;
193 	} else
194 		return (void*)(intptr_t)errno;
195 }
196 
197 /*
198  * VOP_LOOKUP should discard attributes returned by the server if they were
199  * modified by another VOP while the VOP_LOOKUP was in progress.
200  *
201  * Sequence of operations:
202  * * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock
203  *   exclusively.
204  * * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the
205  *   server.  The server replies with the old file length.  Thread 2 blocks
206  *   waiting for the vnode lock.
207  * * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the
208  *   file's size and updates the attribute cache.  Then it releases the vnode
209  *   lock.
210  * * Thread 2 acquires the vnode lock.  At this point it must not add the
211  *   now-stale file size to the attribute cache.
212  *
213  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
214  */
TEST_P(LastLocalModify,lookup)215 TEST_P(LastLocalModify, lookup)
216 {
217 	const char FULLPATH[] = "mountpoint/some_file.txt";
218 	const char RELPATH[] = "some_file.txt";
219 	Sequence seq;
220 	uint64_t ino = 3;
221 	uint64_t mutator_unique;
222 	const uint64_t oldsize = 10;
223 	const uint64_t newsize = 15;
224 	pthread_t th0;
225 	void *thr0_value;
226 	struct stat sb;
227 	static sem_t sem;
228 	Mutator mutator;
229 	uint32_t mutator_op;
230 	size_t mutator_size;
231 
232 	mutator = writer_from_str(GetParam());
233 	mutator_op = fuse_op_from_mutator(mutator);
234 
235 	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
236 
237 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
238 	.InSequence(seq)
239 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
240 		/* Called by the mutator, caches attributes but not entries */
241 		SET_OUT_HEADER_LEN(out, entry);
242 		out.body.entry.nodeid = ino;
243 		out.body.entry.attr.size = oldsize;
244 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
245 		out.body.entry.attr.ino = ino;
246 		out.body.entry.attr.mode = S_IFREG | 0644;
247 	})));
248 	expect_open(ino, 0, 1);
249 	EXPECT_CALL(*m_mock, process(
250 		ResultOf([=](auto in) {
251 			return (in.header.opcode == mutator_op &&
252 				in.header.nodeid == ino);
253 		}, Eq(true)),
254 		_)
255 	).InSequence(seq)
256 	.WillOnce(Invoke([&](auto in, auto &out __unused) {
257 		/*
258 		 * The mutator changes the file size, but in order to simulate
259 		 * a race, don't reply.  Instead, just save the unique for
260 		 * later.
261 		 */
262 		mutator_unique = in.header.unique;
263 		switch(mutator) {
264 		case VOP_WRITE:
265 			mutator_size = in.body.write.size;
266 			break;
267 		case VOP_COPY_FILE_RANGE:
268 			mutator_size = in.body.copy_file_range.len;
269 			break;
270 		default:
271 			break;
272 		}
273 		/* Allow the lookup thread to proceed */
274 		sem_post(&sem);
275 	}));
276 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
277 	.InSequence(seq)
278 	.WillOnce(Invoke([&](auto in __unused, auto& out) {
279 		std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
280 		std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
281 
282 		/* First complete the lookup request, returning the old size */
283 		out0->header.unique = in.header.unique;
284 		SET_OUT_HEADER_LEN(*out0, entry);
285 		out0->body.entry.attr.mode = S_IFREG | 0644;
286 		out0->body.entry.nodeid = ino;
287 		out0->body.entry.attr.ino = ino;
288 		out0->body.entry.entry_valid = UINT64_MAX;
289 		out0->body.entry.attr_valid = UINT64_MAX;
290 		out0->body.entry.attr.size = oldsize;
291 		out.push_back(std::move(out0));
292 
293 		/* Then, respond to the mutator request */
294 		out1->header.unique = mutator_unique;
295 		switch(mutator) {
296 		case VOP_ALLOCATE:
297 			out1->header.error = 0;
298 			out1->header.len = sizeof(out1->header);
299 			break;
300 		case VOP_COPY_FILE_RANGE:
301 			SET_OUT_HEADER_LEN(*out1, write);
302 			out1->body.write.size = mutator_size;
303 			break;
304 		case VOP_SETATTR:
305 			SET_OUT_HEADER_LEN(*out1, attr);
306 			out1->body.attr.attr.ino = ino;
307 			out1->body.attr.attr.mode = S_IFREG | 0644;
308 			out1->body.attr.attr.size = newsize;	// Changed size
309 			out1->body.attr.attr_valid = UINT64_MAX;
310 			break;
311 		case VOP_WRITE:
312 			SET_OUT_HEADER_LEN(*out1, write);
313 			out1->body.write.size = mutator_size;
314 			break;
315 		}
316 		out.push_back(std::move(out1));
317 	}));
318 
319 	/* Start the mutator thread */
320 	switch(mutator) {
321 	case VOP_ALLOCATE:
322 		ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
323 			NULL)) << strerror(errno);
324 		break;
325 	case VOP_COPY_FILE_RANGE:
326 		ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
327 			NULL)) << strerror(errno);
328 		break;
329 	case VOP_SETATTR:
330 		ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))
331 			<< strerror(errno);
332 		break;
333 	case VOP_WRITE:
334 		ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))
335 			<< strerror(errno);
336 		break;
337 	}
338 
339 
340 	/* Wait for FUSE_SETATTR to be sent */
341 	sem_wait(&sem);
342 
343 	/* Lookup again, which will race with the mutator */
344 	ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
345 	ASSERT_EQ((off_t)newsize, sb.st_size);
346 
347 	/* ftruncate should've completed without error */
348 	pthread_join(th0, &thr0_value);
349 	EXPECT_EQ(0, (intptr_t)thr0_value);
350 }
351 
352 /*
353  * VFS_VGET should discard attributes returned by the server if they were
354  * modified by another VOP while the VFS_VGET was in progress.
355  *
356  * Sequence of operations:
357  * * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP
358  * * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock
359  *   exclusively and issues a FUSE op like FUSE_SETATTR.
360  * * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks
361  *   waiting for the vnode lock.
362  * * Thread 2's FUSE op returns, and that thread sets the file's new size
363  *   in the attribute cache.  Finally it releases the vnode lock.
364  * * The vnode lock acquired, thread 1 must not overwrite the attr cache's size
365  *   with the old value.
366  *
367  * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071
368  */
TEST_P(LastLocalModify,vfs_vget)369 TEST_P(LastLocalModify, vfs_vget)
370 {
371 	const char FULLPATH[] = "mountpoint/some_file.txt";
372 	const char RELPATH[] = "some_file.txt";
373 	Sequence seq;
374 	uint64_t ino = 3;
375 	uint64_t lookup_unique;
376 	const uint64_t oldsize = 10;
377 	const uint64_t newsize = 15;
378 	pthread_t th0;
379 	void *thr0_value;
380 	struct stat sb;
381 	static sem_t sem;
382 	fhandle_t fhp;
383 	Mutator mutator;
384 	uint32_t mutator_op;
385 
386 	if (geteuid() != 0)
387 		GTEST_SKIP() << "This test requires a privileged user";
388 
389 	mutator = writer_from_str(GetParam());
390 	mutator_op = fuse_op_from_mutator(mutator);
391 
392 	ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
393 
394 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
395 	.Times(1)
396 	.InSequence(seq)
397 	.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
398 	{
399 		/* Called by getfh, caches attributes but not entries */
400 		SET_OUT_HEADER_LEN(out, entry);
401 		out.body.entry.nodeid = ino;
402 		out.body.entry.attr.size = oldsize;
403 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
404 		out.body.entry.attr.ino = ino;
405 		out.body.entry.attr.mode = S_IFREG | 0644;
406 	})));
407 	EXPECT_LOOKUP(ino, ".")
408 	.InSequence(seq)
409 	.WillOnce(Invoke([&](auto in, auto &out __unused) {
410 		/* Called by fhstat.  Block to simulate a race */
411 		lookup_unique = in.header.unique;
412 		sem_post(&sem);
413 	}));
414 
415 	EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
416 	.Times(1)
417 	.InSequence(seq)
418 	.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)
419 	{
420 		/* Called by VOP_SETATTR, caches attributes but not entries */
421 		SET_OUT_HEADER_LEN(out, entry);
422 		out.body.entry.nodeid = ino;
423 		out.body.entry.attr.size = oldsize;
424 		out.body.entry.attr_valid_nsec = NAP_NS / 2;
425 		out.body.entry.attr.ino = ino;
426 		out.body.entry.attr.mode = S_IFREG | 0644;
427 	})));
428 
429 	/* Called by the mutator thread */
430 	expect_open(ino, 0, 1);
431 
432 	EXPECT_CALL(*m_mock, process(
433 		ResultOf([=](auto in) {
434 			return (in.header.opcode == mutator_op &&
435 				in.header.nodeid == ino);
436 		}, Eq(true)),
437 		_)
438 	).InSequence(seq)
439 	.WillOnce(Invoke([&](auto in __unused, auto& out) {
440 		std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);
441 		std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);
442 
443 		/* First complete the lookup request, returning the old size */
444 		out0->header.unique = lookup_unique;
445 		SET_OUT_HEADER_LEN(*out0, entry);
446 		out0->body.entry.attr.mode = S_IFREG | 0644;
447 		out0->body.entry.nodeid = ino;
448 		out0->body.entry.attr.ino = ino;
449 		out0->body.entry.entry_valid = UINT64_MAX;
450 		out0->body.entry.attr_valid = UINT64_MAX;
451 		out0->body.entry.attr.size = oldsize;
452 		out.push_back(std::move(out0));
453 
454 		/* Then, respond to the mutator request */
455 		out1->header.unique = in.header.unique;
456 		switch(mutator) {
457 		case VOP_ALLOCATE:
458 			out1->header.error = 0;
459 			out1->header.len = sizeof(out1->header);
460 			break;
461 		case VOP_COPY_FILE_RANGE:
462 			SET_OUT_HEADER_LEN(*out1, write);
463 			out1->body.write.size = in.body.copy_file_range.len;
464 			break;
465 		case VOP_SETATTR:
466 			SET_OUT_HEADER_LEN(*out1, attr);
467 			out1->body.attr.attr.ino = ino;
468 			out1->body.attr.attr.mode = S_IFREG | 0644;
469 			out1->body.attr.attr.size = newsize;	// Changed size
470 			out1->body.attr.attr_valid = UINT64_MAX;
471 			break;
472 		case VOP_WRITE:
473 			SET_OUT_HEADER_LEN(*out1, write);
474 			out1->body.write.size = in.body.write.size;
475 			break;
476 		}
477 		out.push_back(std::move(out1));
478 	}));
479 
480 	/* First get a file handle */
481 	ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
482 
483 	/* Start the mutator thread */
484 	switch(mutator) {
485 	case VOP_ALLOCATE:
486 		ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,
487 			(void*)&sem)) << strerror(errno);
488 		break;
489 	case VOP_COPY_FILE_RANGE:
490 		ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,
491 			(void*)&sem)) << strerror(errno);
492 		break;
493 	case VOP_SETATTR:
494 		ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,
495 			(void*)&sem)) << strerror(errno);
496 		break;
497 	case VOP_WRITE:
498 		ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))
499 			<< strerror(errno);
500 		break;
501 	}
502 
503 	/* Lookup again, which will race with setattr */
504 	ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
505 
506 	ASSERT_EQ((off_t)newsize, sb.st_size);
507 
508 	/* mutator should've completed without error */
509 	pthread_join(th0, &thr0_value);
510 	EXPECT_EQ(0, (intptr_t)thr0_value);
511 }
512 
513 
514 INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify,
515 	Values(
516 		"VOP_ALLOCATE",
517 		"VOP_COPY_FILE_RANGE",
518 		"VOP_SETATTR",
519 		"VOP_WRITE"
520 	)
521 );
522