Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/tests/sys/fs/fusefs/last_local_modify.cc
39537 views
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
/*
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
*/
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
*/
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
523