Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/tests/sys/fs/fusefs/notify.cc
103859 views
1
/*-
2
* SPDX-License-Identifier: BSD-2-Clause
3
*
4
* Copyright (c) 2019 The FreeBSD Foundation
5
*
6
* This software was developed by BFF Storage Systems, LLC under sponsorship
7
* from the FreeBSD Foundation.
8
*
9
* Redistribution and use in source and binary forms, with or without
10
* modification, are permitted provided that the following conditions
11
* are met:
12
* 1. Redistributions of source code must retain the above copyright
13
* notice, this list of conditions and the following disclaimer.
14
* 2. Redistributions in binary form must reproduce the above copyright
15
* notice, this list of conditions and the following disclaimer in the
16
* documentation and/or other materials provided with the distribution.
17
*
18
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28
* SUCH DAMAGE.
29
*/
30
31
extern "C" {
32
#include <sys/types.h>
33
34
#include <fcntl.h>
35
#include <pthread.h>
36
}
37
38
#include "mockfs.hh"
39
#include "utils.hh"
40
41
using namespace testing;
42
43
/*
44
* FUSE asynchonous notification
45
*
46
* FUSE servers can send unprompted notification messages for things like cache
47
* invalidation. This file tests our client's handling of those messages.
48
*/
49
50
class Notify: public FuseTest {
51
public:
52
/* Ignore an optional FUSE_FSYNC */
53
void maybe_expect_fsync(uint64_t ino)
54
{
55
EXPECT_CALL(*m_mock, process(
56
ResultOf([=](auto in) {
57
return (in.header.opcode == FUSE_FSYNC &&
58
in.header.nodeid == ino);
59
}, Eq(true)),
60
_)
61
).WillOnce(Invoke(ReturnErrno(0)));
62
}
63
64
void expect_lookup(uint64_t parent, const char *relpath, uint64_t ino,
65
off_t size, Sequence &seq)
66
{
67
EXPECT_LOOKUP(parent, relpath)
68
.InSequence(seq)
69
.WillOnce(Invoke(
70
ReturnImmediate([=](auto in __unused, auto& out) {
71
SET_OUT_HEADER_LEN(out, entry);
72
out.body.entry.attr.mode = S_IFREG | 0644;
73
out.body.entry.nodeid = ino;
74
out.body.entry.attr.ino = ino;
75
out.body.entry.attr.nlink = 1;
76
out.body.entry.attr.size = size;
77
out.body.entry.attr_valid = UINT64_MAX;
78
out.body.entry.entry_valid = UINT64_MAX;
79
})));
80
}
81
};
82
83
class NotifyWriteback: public Notify {
84
public:
85
virtual void SetUp() {
86
m_init_flags |= FUSE_WRITEBACK_CACHE;
87
m_async = true;
88
Notify::SetUp();
89
if (IsSkipped())
90
return;
91
}
92
93
void expect_write(uint64_t ino, uint64_t offset, uint64_t size,
94
const void *contents)
95
{
96
FuseTest::expect_write(ino, offset, size, size, 0, 0, contents);
97
}
98
99
};
100
101
struct inval_entry_args {
102
MockFS *mock;
103
ino_t parent;
104
const char *name;
105
size_t namelen;
106
};
107
108
static void* inval_entry(void* arg) {
109
const struct inval_entry_args *iea = (struct inval_entry_args*)arg;
110
ssize_t r;
111
112
r = iea->mock->notify_inval_entry(iea->parent, iea->name, iea->namelen);
113
if (r >= 0)
114
return 0;
115
else
116
return (void*)(intptr_t)errno;
117
}
118
119
struct inval_inode_args {
120
MockFS *mock;
121
ino_t ino;
122
off_t off;
123
ssize_t len;
124
};
125
126
struct store_args {
127
MockFS *mock;
128
ino_t nodeid;
129
off_t offset;
130
ssize_t size;
131
const void* data;
132
};
133
134
static void* inval_inode(void* arg) {
135
const struct inval_inode_args *iia = (struct inval_inode_args*)arg;
136
ssize_t r;
137
138
r = iia->mock->notify_inval_inode(iia->ino, iia->off, iia->len);
139
if (r >= 0)
140
return 0;
141
else
142
return (void*)(intptr_t)errno;
143
}
144
145
static void* store(void* arg) {
146
const struct store_args *sa = (struct store_args*)arg;
147
ssize_t r;
148
149
r = sa->mock->notify_store(sa->nodeid, sa->offset, sa->data, sa->size);
150
if (r >= 0)
151
return 0;
152
else
153
return (void*)(intptr_t)errno;
154
}
155
156
/* Invalidate a nonexistent entry */
157
TEST_F(Notify, inval_entry_nonexistent)
158
{
159
const static char *name = "foo";
160
struct inval_entry_args iea;
161
void *thr0_value;
162
pthread_t th0;
163
164
iea.mock = m_mock;
165
iea.parent = FUSE_ROOT_ID;
166
iea.name = name;
167
iea.namelen = strlen(name);
168
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
169
<< strerror(errno);
170
pthread_join(th0, &thr0_value);
171
/* It's not an error for an entry to not be cached */
172
EXPECT_EQ(0, (intptr_t)thr0_value);
173
}
174
175
/* Invalidate a cached entry */
176
TEST_F(Notify, inval_entry)
177
{
178
const static char FULLPATH[] = "mountpoint/foo";
179
const static char RELPATH[] = "foo";
180
struct inval_entry_args iea;
181
struct stat sb;
182
void *thr0_value;
183
uint64_t ino0 = 42;
184
uint64_t ino1 = 43;
185
Sequence seq;
186
pthread_t th0;
187
188
expect_lookup(FUSE_ROOT_ID, RELPATH, ino0, 0, seq);
189
expect_lookup(FUSE_ROOT_ID, RELPATH, ino1, 0, seq);
190
191
/* Fill the entry cache */
192
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
193
EXPECT_EQ(ino0, sb.st_ino);
194
195
/* Now invalidate the entry */
196
iea.mock = m_mock;
197
iea.parent = FUSE_ROOT_ID;
198
iea.name = RELPATH;
199
iea.namelen = strlen(RELPATH);
200
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
201
<< strerror(errno);
202
pthread_join(th0, &thr0_value);
203
EXPECT_EQ(0, (intptr_t)thr0_value);
204
205
/* The second lookup should return the alternate ino */
206
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
207
EXPECT_EQ(ino1, sb.st_ino);
208
}
209
210
/*
211
* Invalidate a cached entry beneath the root, which uses a slightly different
212
* code path.
213
*/
214
TEST_F(Notify, inval_entry_below_root)
215
{
216
const static char FULLPATH[] = "mountpoint/some_dir/foo";
217
const static char DNAME[] = "some_dir";
218
const static char FNAME[] = "foo";
219
struct inval_entry_args iea;
220
struct stat sb;
221
void *thr0_value;
222
uint64_t dir_ino = 41;
223
uint64_t ino0 = 42;
224
uint64_t ino1 = 43;
225
Sequence seq;
226
pthread_t th0;
227
228
EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
229
.WillOnce(Invoke(
230
ReturnImmediate([=](auto in __unused, auto& out) {
231
SET_OUT_HEADER_LEN(out, entry);
232
out.body.entry.attr.mode = S_IFDIR | 0755;
233
out.body.entry.nodeid = dir_ino;
234
out.body.entry.attr.nlink = 2;
235
out.body.entry.attr_valid = UINT64_MAX;
236
out.body.entry.entry_valid = UINT64_MAX;
237
})));
238
expect_lookup(dir_ino, FNAME, ino0, 0, seq);
239
expect_lookup(dir_ino, FNAME, ino1, 0, seq);
240
241
/* Fill the entry cache */
242
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
243
EXPECT_EQ(ino0, sb.st_ino);
244
245
/* Now invalidate the entry */
246
iea.mock = m_mock;
247
iea.parent = dir_ino;
248
iea.name = FNAME;
249
iea.namelen = strlen(FNAME);
250
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
251
<< strerror(errno);
252
pthread_join(th0, &thr0_value);
253
EXPECT_EQ(0, (intptr_t)thr0_value);
254
255
/* The second lookup should return the alternate ino */
256
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
257
EXPECT_EQ(ino1, sb.st_ino);
258
}
259
260
/* Invalidating an entry invalidates the parent directory's attributes */
261
TEST_F(Notify, inval_entry_invalidates_parent_attrs)
262
{
263
const static char FULLPATH[] = "mountpoint/foo";
264
const static char RELPATH[] = "foo";
265
struct inval_entry_args iea;
266
struct stat sb;
267
void *thr0_value;
268
uint64_t ino = 42;
269
Sequence seq;
270
pthread_t th0;
271
272
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
273
EXPECT_CALL(*m_mock, process(
274
ResultOf([=](auto in) {
275
return (in.header.opcode == FUSE_GETATTR &&
276
in.header.nodeid == FUSE_ROOT_ID);
277
}, Eq(true)),
278
_)
279
).Times(2)
280
.WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
281
SET_OUT_HEADER_LEN(out, attr);
282
out.body.attr.attr.mode = S_IFDIR | 0755;
283
out.body.attr.attr_valid = UINT64_MAX;
284
})));
285
286
/* Fill the attr and entry cache */
287
ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
288
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
289
290
/* Now invalidate the entry */
291
iea.mock = m_mock;
292
iea.parent = FUSE_ROOT_ID;
293
iea.name = RELPATH;
294
iea.namelen = strlen(RELPATH);
295
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
296
<< strerror(errno);
297
pthread_join(th0, &thr0_value);
298
EXPECT_EQ(0, (intptr_t)thr0_value);
299
300
/* /'s attribute cache should be cleared */
301
ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
302
}
303
304
305
TEST_F(Notify, inval_inode_nonexistent)
306
{
307
struct inval_inode_args iia;
308
ino_t ino = 42;
309
void *thr0_value;
310
pthread_t th0;
311
312
iia.mock = m_mock;
313
iia.ino = ino;
314
iia.off = 0;
315
iia.len = 0;
316
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
317
<< strerror(errno);
318
pthread_join(th0, &thr0_value);
319
/* It's not an error for an inode to not be cached */
320
EXPECT_EQ(0, (intptr_t)thr0_value);
321
}
322
323
TEST_F(Notify, inval_inode_with_clean_cache)
324
{
325
const static char FULLPATH[] = "mountpoint/foo";
326
const static char RELPATH[] = "foo";
327
const char CONTENTS0[] = "abcdefgh";
328
const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
329
struct inval_inode_args iia;
330
struct stat sb;
331
ino_t ino = 42;
332
void *thr0_value;
333
Sequence seq;
334
uid_t uid = 12345;
335
pthread_t th0;
336
ssize_t size0 = sizeof(CONTENTS0);
337
ssize_t size1 = sizeof(CONTENTS1);
338
char buf[80];
339
int fd;
340
341
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size0, seq);
342
expect_open(ino, 0, 1);
343
EXPECT_CALL(*m_mock, process(
344
ResultOf([=](auto in) {
345
return (in.header.opcode == FUSE_GETATTR &&
346
in.header.nodeid == ino);
347
}, Eq(true)),
348
_)
349
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
350
SET_OUT_HEADER_LEN(out, attr);
351
out.body.attr.attr.mode = S_IFREG | 0644;
352
out.body.attr.attr_valid = UINT64_MAX;
353
out.body.attr.attr.size = size1;
354
out.body.attr.attr.uid = uid;
355
})));
356
expect_read(ino, 0, size0, size0, CONTENTS0);
357
expect_read(ino, 0, size1, size1, CONTENTS1);
358
359
/* Fill the data cache */
360
fd = open(FULLPATH, O_RDWR);
361
ASSERT_LE(0, fd) << strerror(errno);
362
ASSERT_EQ(size0, read(fd, buf, size0)) << strerror(errno);
363
EXPECT_EQ(0, memcmp(buf, CONTENTS0, size0));
364
365
/* Evict the data cache */
366
iia.mock = m_mock;
367
iia.ino = ino;
368
iia.off = 0;
369
iia.len = 0;
370
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
371
<< strerror(errno);
372
pthread_join(th0, &thr0_value);
373
EXPECT_EQ(0, (intptr_t)thr0_value);
374
375
/* cache attributes were purged; this will trigger a new GETATTR */
376
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
377
EXPECT_EQ(uid, sb.st_uid);
378
EXPECT_EQ(size1, sb.st_size);
379
380
/* This read should not be serviced by cache */
381
ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
382
ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
383
EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
384
385
leak(fd);
386
}
387
388
/*
389
* Attempting to invalidate an entry or inode after unmounting should fail, but
390
* nothing bad should happen.
391
* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519
392
*/
393
TEST_F(Notify, notify_after_unmount)
394
{
395
const static char *name = "foo";
396
struct inval_entry_args iea;
397
398
expect_destroy(0);
399
400
m_mock->unmount();
401
402
iea.mock = m_mock;
403
iea.parent = FUSE_ROOT_ID;
404
iea.name = name;
405
iea.namelen = strlen(name);
406
iea.mock->notify_inval_entry(iea.parent, iea.name, iea.namelen, ENODEV);
407
}
408
409
/* FUSE_NOTIFY_STORE with a file that's not in the entry cache */
410
/* disabled because FUSE_NOTIFY_STORE is not yet implemented */
411
TEST_F(Notify, DISABLED_store_nonexistent)
412
{
413
struct store_args sa;
414
ino_t ino = 42;
415
void *thr0_value;
416
pthread_t th0;
417
418
sa.mock = m_mock;
419
sa.nodeid = ino;
420
sa.offset = 0;
421
sa.size = 0;
422
ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
423
pthread_join(th0, &thr0_value);
424
/* It's not an error for a file to be unknown to the kernel */
425
EXPECT_EQ(0, (intptr_t)thr0_value);
426
}
427
428
/* Store data into for a file that does not yet have anything cached */
429
/* disabled because FUSE_NOTIFY_STORE is not yet implemented */
430
TEST_F(Notify, DISABLED_store_with_blank_cache)
431
{
432
const static char FULLPATH[] = "mountpoint/foo";
433
const static char RELPATH[] = "foo";
434
const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
435
struct store_args sa;
436
ino_t ino = 42;
437
void *thr0_value;
438
Sequence seq;
439
pthread_t th0;
440
ssize_t size1 = sizeof(CONTENTS1);
441
char buf[80];
442
int fd;
443
444
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size1, seq);
445
expect_open(ino, 0, 1);
446
447
/* Fill the data cache */
448
fd = open(FULLPATH, O_RDWR);
449
ASSERT_LE(0, fd) << strerror(errno);
450
451
/* Evict the data cache */
452
sa.mock = m_mock;
453
sa.nodeid = ino;
454
sa.offset = 0;
455
sa.size = size1;
456
sa.data = (const void*)CONTENTS1;
457
ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
458
pthread_join(th0, &thr0_value);
459
EXPECT_EQ(0, (intptr_t)thr0_value);
460
461
/* This read should be serviced by cache */
462
ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
463
EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
464
465
leak(fd);
466
}
467
468
TEST_F(NotifyWriteback, inval_inode_with_dirty_cache)
469
{
470
const static char FULLPATH[] = "mountpoint/foo";
471
const static char RELPATH[] = "foo";
472
const char CONTENTS[] = "abcdefgh";
473
struct inval_inode_args iia;
474
ino_t ino = 42;
475
void *thr0_value;
476
Sequence seq;
477
pthread_t th0;
478
ssize_t bufsize = sizeof(CONTENTS);
479
int fd;
480
481
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
482
expect_open(ino, 0, 1);
483
484
/* Fill the data cache */
485
fd = open(FULLPATH, O_RDWR);
486
ASSERT_LE(0, fd);
487
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
488
489
expect_write(ino, 0, bufsize, CONTENTS);
490
/*
491
* The FUSE protocol does not require an fsync here, but FreeBSD's
492
* bufobj_invalbuf sends it anyway
493
*/
494
maybe_expect_fsync(ino);
495
496
/* Evict the data cache */
497
iia.mock = m_mock;
498
iia.ino = ino;
499
iia.off = 0;
500
iia.len = 0;
501
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
502
<< strerror(errno);
503
pthread_join(th0, &thr0_value);
504
EXPECT_EQ(0, (intptr_t)thr0_value);
505
506
leak(fd);
507
}
508
509
TEST_F(NotifyWriteback, inval_inode_attrs_only)
510
{
511
const static char FULLPATH[] = "mountpoint/foo";
512
const static char RELPATH[] = "foo";
513
const char CONTENTS[] = "abcdefgh";
514
struct inval_inode_args iia;
515
struct stat sb;
516
uid_t uid = 12345;
517
ino_t ino = 42;
518
void *thr0_value;
519
Sequence seq;
520
pthread_t th0;
521
ssize_t bufsize = sizeof(CONTENTS);
522
int fd;
523
524
expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
525
expect_open(ino, 0, 1);
526
EXPECT_CALL(*m_mock, process(
527
ResultOf([=](auto in) {
528
return (in.header.opcode == FUSE_WRITE);
529
}, Eq(true)),
530
_)
531
).Times(0);
532
EXPECT_CALL(*m_mock, process(
533
ResultOf([=](auto in) {
534
return (in.header.opcode == FUSE_GETATTR &&
535
in.header.nodeid == ino);
536
}, Eq(true)),
537
_)
538
).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
539
SET_OUT_HEADER_LEN(out, attr);
540
out.body.attr.attr.mode = S_IFREG | 0644;
541
out.body.attr.attr_valid = UINT64_MAX;
542
out.body.attr.attr.size = bufsize;
543
out.body.attr.attr.uid = uid;
544
})));
545
546
/* Fill the data cache */
547
fd = open(FULLPATH, O_RDWR);
548
ASSERT_LE(0, fd) << strerror(errno);
549
ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
550
551
/* Evict the attributes, but not data cache */
552
iia.mock = m_mock;
553
iia.ino = ino;
554
iia.off = -1;
555
iia.len = 0;
556
ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
557
<< strerror(errno);
558
pthread_join(th0, &thr0_value);
559
EXPECT_EQ(0, (intptr_t)thr0_value);
560
561
/* cache attributes were been purged; this will trigger a new GETATTR */
562
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
563
EXPECT_EQ(uid, sb.st_uid);
564
EXPECT_EQ(bufsize, sb.st_size);
565
566
leak(fd);
567
}
568
569
/*
570
* Attempting asynchronous invalidation of an Entry before mounting the file
571
* system should fail, but nothing bad should happen.
572
*
573
* Note that invalidating an inode before mount goes through the same path, and
574
* is not separately tested.
575
*
576
* https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=290519
577
*/
578
TEST(PreMount, inval_entry_before_mount)
579
{
580
const static char name[] = "foo";
581
size_t namelen = strlen(name);
582
struct mockfs_buf_out *out;
583
int r;
584
int fuse_fd;
585
586
fuse_fd = open("/dev/fuse", O_CLOEXEC | O_RDWR);
587
ASSERT_GE(fuse_fd, 0) << strerror(errno);
588
589
out = new mockfs_buf_out;
590
out->header.unique = 0; /* 0 means asynchronous notification */
591
out->header.error = FUSE_NOTIFY_INVAL_ENTRY;
592
out->body.inval_entry.parent = FUSE_ROOT_ID;
593
out->body.inval_entry.namelen = namelen;
594
strlcpy((char*)&out->body.bytes + sizeof(out->body.inval_entry),
595
name, sizeof(out->body.bytes) - sizeof(out->body.inval_entry));
596
out->header.len = sizeof(out->header) + sizeof(out->body.inval_entry) +
597
namelen;
598
r = write(fuse_fd, out, out->header.len);
599
EXPECT_EQ(-1, r);
600
EXPECT_EQ(ENODEV, errno);
601
delete out;
602
}
603
604