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 #include <sys/sysctl.h>
34
35 #include <fcntl.h>
36 #include <pthread.h>
37 }
38
39 #include "mockfs.hh"
40 #include "utils.hh"
41
42 using namespace testing;
43
44 /*
45 * FUSE asynchonous notification
46 *
47 * FUSE servers can send unprompted notification messages for things like cache
48 * invalidation. This file tests our client's handling of those messages.
49 */
50
51 class Notify: public FuseTest {
52 public:
53 /* Ignore an optional FUSE_FSYNC */
maybe_expect_fsync(uint64_t ino)54 void maybe_expect_fsync(uint64_t ino)
55 {
56 EXPECT_CALL(*m_mock, process(
57 ResultOf([=](auto in) {
58 return (in.header.opcode == FUSE_FSYNC &&
59 in.header.nodeid == ino);
60 }, Eq(true)),
61 _)
62 ).WillOnce(Invoke(ReturnErrno(0)));
63 }
64
expect_lookup(uint64_t parent,const char * relpath,uint64_t ino,off_t size,Sequence & seq)65 void expect_lookup(uint64_t parent, const char *relpath, uint64_t ino,
66 off_t size, Sequence &seq)
67 {
68 EXPECT_LOOKUP(parent, relpath)
69 .InSequence(seq)
70 .WillOnce(Invoke(
71 ReturnImmediate([=](auto in __unused, auto& out) {
72 SET_OUT_HEADER_LEN(out, entry);
73 out.body.entry.attr.mode = S_IFREG | 0644;
74 out.body.entry.nodeid = ino;
75 out.body.entry.attr.ino = ino;
76 out.body.entry.attr.nlink = 1;
77 out.body.entry.attr.size = size;
78 out.body.entry.attr_valid = UINT64_MAX;
79 out.body.entry.entry_valid = UINT64_MAX;
80 })));
81 }
82 };
83
84 class NotifyWriteback: public Notify {
85 public:
SetUp()86 virtual void SetUp() {
87 m_init_flags |= FUSE_WRITEBACK_CACHE;
88 m_async = true;
89 Notify::SetUp();
90 if (IsSkipped())
91 return;
92 }
93
expect_write(uint64_t ino,uint64_t offset,uint64_t size,const void * contents)94 void expect_write(uint64_t ino, uint64_t offset, uint64_t size,
95 const void *contents)
96 {
97 FuseTest::expect_write(ino, offset, size, size, 0, 0, contents);
98 }
99
100 };
101
102 struct inval_entry_args {
103 MockFS *mock;
104 ino_t parent;
105 const char *name;
106 size_t namelen;
107 };
108
inval_entry(void * arg)109 static void* inval_entry(void* arg) {
110 const struct inval_entry_args *iea = (struct inval_entry_args*)arg;
111 ssize_t r;
112
113 r = iea->mock->notify_inval_entry(iea->parent, iea->name, iea->namelen);
114 if (r >= 0)
115 return 0;
116 else
117 return (void*)(intptr_t)errno;
118 }
119
120 struct inval_inode_args {
121 MockFS *mock;
122 ino_t ino;
123 off_t off;
124 ssize_t len;
125 };
126
127 struct store_args {
128 MockFS *mock;
129 ino_t nodeid;
130 off_t offset;
131 ssize_t size;
132 const void* data;
133 };
134
inval_inode(void * arg)135 static void* inval_inode(void* arg) {
136 const struct inval_inode_args *iia = (struct inval_inode_args*)arg;
137 ssize_t r;
138
139 r = iia->mock->notify_inval_inode(iia->ino, iia->off, iia->len);
140 if (r >= 0)
141 return 0;
142 else
143 return (void*)(intptr_t)errno;
144 }
145
store(void * arg)146 static void* store(void* arg) {
147 const struct store_args *sa = (struct store_args*)arg;
148 ssize_t r;
149
150 r = sa->mock->notify_store(sa->nodeid, sa->offset, sa->data, sa->size);
151 if (r >= 0)
152 return 0;
153 else
154 return (void*)(intptr_t)errno;
155 }
156
157 /* Invalidate a nonexistent entry */
TEST_F(Notify,inval_entry_nonexistent)158 TEST_F(Notify, inval_entry_nonexistent)
159 {
160 const static char *name = "foo";
161 struct inval_entry_args iea;
162 void *thr0_value;
163 pthread_t th0;
164
165 iea.mock = m_mock;
166 iea.parent = FUSE_ROOT_ID;
167 iea.name = name;
168 iea.namelen = strlen(name);
169 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
170 << strerror(errno);
171 pthread_join(th0, &thr0_value);
172 /* It's not an error for an entry to not be cached */
173 EXPECT_EQ(0, (intptr_t)thr0_value);
174 }
175
176 /* Invalidate a cached entry */
TEST_F(Notify,inval_entry)177 TEST_F(Notify, inval_entry)
178 {
179 const static char FULLPATH[] = "mountpoint/foo";
180 const static char RELPATH[] = "foo";
181 struct inval_entry_args iea;
182 struct stat sb;
183 void *thr0_value;
184 uint64_t ino0 = 42;
185 uint64_t ino1 = 43;
186 Sequence seq;
187 pthread_t th0;
188
189 expect_lookup(FUSE_ROOT_ID, RELPATH, ino0, 0, seq);
190 expect_lookup(FUSE_ROOT_ID, RELPATH, ino1, 0, seq);
191
192 /* Fill the entry cache */
193 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
194 EXPECT_EQ(ino0, sb.st_ino);
195
196 /* Now invalidate the entry */
197 iea.mock = m_mock;
198 iea.parent = FUSE_ROOT_ID;
199 iea.name = RELPATH;
200 iea.namelen = strlen(RELPATH);
201 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
202 << strerror(errno);
203 pthread_join(th0, &thr0_value);
204 EXPECT_EQ(0, (intptr_t)thr0_value);
205
206 /* The second lookup should return the alternate ino */
207 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
208 EXPECT_EQ(ino1, sb.st_ino);
209 }
210
211 /*
212 * Invalidate a cached entry beneath the root, which uses a slightly different
213 * code path.
214 */
TEST_F(Notify,inval_entry_below_root)215 TEST_F(Notify, inval_entry_below_root)
216 {
217 const static char FULLPATH[] = "mountpoint/some_dir/foo";
218 const static char DNAME[] = "some_dir";
219 const static char FNAME[] = "foo";
220 struct inval_entry_args iea;
221 struct stat sb;
222 void *thr0_value;
223 uint64_t dir_ino = 41;
224 uint64_t ino0 = 42;
225 uint64_t ino1 = 43;
226 Sequence seq;
227 pthread_t th0;
228
229 EXPECT_LOOKUP(FUSE_ROOT_ID, DNAME)
230 .WillOnce(Invoke(
231 ReturnImmediate([=](auto in __unused, auto& out) {
232 SET_OUT_HEADER_LEN(out, entry);
233 out.body.entry.attr.mode = S_IFDIR | 0755;
234 out.body.entry.nodeid = dir_ino;
235 out.body.entry.attr.nlink = 2;
236 out.body.entry.attr_valid = UINT64_MAX;
237 out.body.entry.entry_valid = UINT64_MAX;
238 })));
239 expect_lookup(dir_ino, FNAME, ino0, 0, seq);
240 expect_lookup(dir_ino, FNAME, ino1, 0, seq);
241
242 /* Fill the entry cache */
243 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
244 EXPECT_EQ(ino0, sb.st_ino);
245
246 /* Now invalidate the entry */
247 iea.mock = m_mock;
248 iea.parent = dir_ino;
249 iea.name = FNAME;
250 iea.namelen = strlen(FNAME);
251 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
252 << strerror(errno);
253 pthread_join(th0, &thr0_value);
254 EXPECT_EQ(0, (intptr_t)thr0_value);
255
256 /* The second lookup should return the alternate ino */
257 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
258 EXPECT_EQ(ino1, sb.st_ino);
259 }
260
261 /* Invalidating an entry invalidates the parent directory's attributes */
TEST_F(Notify,inval_entry_invalidates_parent_attrs)262 TEST_F(Notify, inval_entry_invalidates_parent_attrs)
263 {
264 const static char FULLPATH[] = "mountpoint/foo";
265 const static char RELPATH[] = "foo";
266 struct inval_entry_args iea;
267 struct stat sb;
268 void *thr0_value;
269 uint64_t ino = 42;
270 Sequence seq;
271 pthread_t th0;
272
273 expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
274 EXPECT_CALL(*m_mock, process(
275 ResultOf([=](auto in) {
276 return (in.header.opcode == FUSE_GETATTR &&
277 in.header.nodeid == FUSE_ROOT_ID);
278 }, Eq(true)),
279 _)
280 ).Times(2)
281 .WillRepeatedly(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
282 SET_OUT_HEADER_LEN(out, attr);
283 out.body.attr.attr.mode = S_IFDIR | 0755;
284 out.body.attr.attr_valid = UINT64_MAX;
285 })));
286
287 /* Fill the attr and entry cache */
288 ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
289 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
290
291 /* Now invalidate the entry */
292 iea.mock = m_mock;
293 iea.parent = FUSE_ROOT_ID;
294 iea.name = RELPATH;
295 iea.namelen = strlen(RELPATH);
296 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_entry, &iea))
297 << strerror(errno);
298 pthread_join(th0, &thr0_value);
299 EXPECT_EQ(0, (intptr_t)thr0_value);
300
301 /* /'s attribute cache should be cleared */
302 ASSERT_EQ(0, stat("mountpoint", &sb)) << strerror(errno);
303 }
304
305
TEST_F(Notify,inval_inode_nonexistent)306 TEST_F(Notify, inval_inode_nonexistent)
307 {
308 struct inval_inode_args iia;
309 ino_t ino = 42;
310 void *thr0_value;
311 pthread_t th0;
312
313 iia.mock = m_mock;
314 iia.ino = ino;
315 iia.off = 0;
316 iia.len = 0;
317 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
318 << strerror(errno);
319 pthread_join(th0, &thr0_value);
320 /* It's not an error for an inode to not be cached */
321 EXPECT_EQ(0, (intptr_t)thr0_value);
322 }
323
TEST_F(Notify,inval_inode_with_clean_cache)324 TEST_F(Notify, inval_inode_with_clean_cache)
325 {
326 const static char FULLPATH[] = "mountpoint/foo";
327 const static char RELPATH[] = "foo";
328 const char CONTENTS0[] = "abcdefgh";
329 const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
330 struct inval_inode_args iia;
331 struct stat sb;
332 ino_t ino = 42;
333 void *thr0_value;
334 Sequence seq;
335 uid_t uid = 12345;
336 pthread_t th0;
337 ssize_t size0 = sizeof(CONTENTS0);
338 ssize_t size1 = sizeof(CONTENTS1);
339 char buf[80];
340 int fd;
341
342 expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size0, seq);
343 expect_open(ino, 0, 1);
344 EXPECT_CALL(*m_mock, process(
345 ResultOf([=](auto in) {
346 return (in.header.opcode == FUSE_GETATTR &&
347 in.header.nodeid == ino);
348 }, Eq(true)),
349 _)
350 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
351 SET_OUT_HEADER_LEN(out, attr);
352 out.body.attr.attr.mode = S_IFREG | 0644;
353 out.body.attr.attr_valid = UINT64_MAX;
354 out.body.attr.attr.size = size1;
355 out.body.attr.attr.uid = uid;
356 })));
357 expect_read(ino, 0, size0, size0, CONTENTS0);
358 expect_read(ino, 0, size1, size1, CONTENTS1);
359
360 /* Fill the data cache */
361 fd = open(FULLPATH, O_RDWR);
362 ASSERT_LE(0, fd) << strerror(errno);
363 ASSERT_EQ(size0, read(fd, buf, size0)) << strerror(errno);
364 EXPECT_EQ(0, memcmp(buf, CONTENTS0, size0));
365
366 /* Evict the data cache */
367 iia.mock = m_mock;
368 iia.ino = ino;
369 iia.off = 0;
370 iia.len = 0;
371 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
372 << strerror(errno);
373 pthread_join(th0, &thr0_value);
374 EXPECT_EQ(0, (intptr_t)thr0_value);
375
376 /* cache attributes were purged; this will trigger a new GETATTR */
377 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
378 EXPECT_EQ(uid, sb.st_uid);
379 EXPECT_EQ(size1, sb.st_size);
380
381 /* This read should not be serviced by cache */
382 ASSERT_EQ(0, lseek(fd, 0, SEEK_SET)) << strerror(errno);
383 ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
384 EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
385
386 leak(fd);
387 }
388
389 /* FUSE_NOTIFY_STORE with a file that's not in the entry cache */
390 /* disabled because FUSE_NOTIFY_STORE is not yet implemented */
TEST_F(Notify,DISABLED_store_nonexistent)391 TEST_F(Notify, DISABLED_store_nonexistent)
392 {
393 struct store_args sa;
394 ino_t ino = 42;
395 void *thr0_value;
396 pthread_t th0;
397
398 sa.mock = m_mock;
399 sa.nodeid = ino;
400 sa.offset = 0;
401 sa.size = 0;
402 ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
403 pthread_join(th0, &thr0_value);
404 /* It's not an error for a file to be unknown to the kernel */
405 EXPECT_EQ(0, (intptr_t)thr0_value);
406 }
407
408 /* Store data into for a file that does not yet have anything cached */
409 /* disabled because FUSE_NOTIFY_STORE is not yet implemented */
TEST_F(Notify,DISABLED_store_with_blank_cache)410 TEST_F(Notify, DISABLED_store_with_blank_cache)
411 {
412 const static char FULLPATH[] = "mountpoint/foo";
413 const static char RELPATH[] = "foo";
414 const char CONTENTS1[] = "ijklmnopqrstuvwxyz";
415 struct store_args sa;
416 ino_t ino = 42;
417 void *thr0_value;
418 Sequence seq;
419 pthread_t th0;
420 ssize_t size1 = sizeof(CONTENTS1);
421 char buf[80];
422 int fd;
423
424 expect_lookup(FUSE_ROOT_ID, RELPATH, ino, size1, seq);
425 expect_open(ino, 0, 1);
426
427 /* Fill the data cache */
428 fd = open(FULLPATH, O_RDWR);
429 ASSERT_LE(0, fd) << strerror(errno);
430
431 /* Evict the data cache */
432 sa.mock = m_mock;
433 sa.nodeid = ino;
434 sa.offset = 0;
435 sa.size = size1;
436 sa.data = (const void*)CONTENTS1;
437 ASSERT_EQ(0, pthread_create(&th0, NULL, store, &sa)) << strerror(errno);
438 pthread_join(th0, &thr0_value);
439 EXPECT_EQ(0, (intptr_t)thr0_value);
440
441 /* This read should be serviced by cache */
442 ASSERT_EQ(size1, read(fd, buf, size1)) << strerror(errno);
443 EXPECT_EQ(0, memcmp(buf, CONTENTS1, size1));
444
445 leak(fd);
446 }
447
TEST_F(NotifyWriteback,inval_inode_with_dirty_cache)448 TEST_F(NotifyWriteback, inval_inode_with_dirty_cache)
449 {
450 const static char FULLPATH[] = "mountpoint/foo";
451 const static char RELPATH[] = "foo";
452 const char CONTENTS[] = "abcdefgh";
453 struct inval_inode_args iia;
454 ino_t ino = 42;
455 void *thr0_value;
456 Sequence seq;
457 pthread_t th0;
458 ssize_t bufsize = sizeof(CONTENTS);
459 int fd;
460
461 expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
462 expect_open(ino, 0, 1);
463
464 /* Fill the data cache */
465 fd = open(FULLPATH, O_RDWR);
466 ASSERT_LE(0, fd);
467 ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
468
469 expect_write(ino, 0, bufsize, CONTENTS);
470 /*
471 * The FUSE protocol does not require an fsync here, but FreeBSD's
472 * bufobj_invalbuf sends it anyway
473 */
474 maybe_expect_fsync(ino);
475
476 /* Evict the data cache */
477 iia.mock = m_mock;
478 iia.ino = ino;
479 iia.off = 0;
480 iia.len = 0;
481 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
482 << strerror(errno);
483 pthread_join(th0, &thr0_value);
484 EXPECT_EQ(0, (intptr_t)thr0_value);
485
486 leak(fd);
487 }
488
TEST_F(NotifyWriteback,inval_inode_attrs_only)489 TEST_F(NotifyWriteback, inval_inode_attrs_only)
490 {
491 const static char FULLPATH[] = "mountpoint/foo";
492 const static char RELPATH[] = "foo";
493 const char CONTENTS[] = "abcdefgh";
494 struct inval_inode_args iia;
495 struct stat sb;
496 uid_t uid = 12345;
497 ino_t ino = 42;
498 void *thr0_value;
499 Sequence seq;
500 pthread_t th0;
501 ssize_t bufsize = sizeof(CONTENTS);
502 int fd;
503
504 expect_lookup(FUSE_ROOT_ID, RELPATH, ino, 0, seq);
505 expect_open(ino, 0, 1);
506 EXPECT_CALL(*m_mock, process(
507 ResultOf([=](auto in) {
508 return (in.header.opcode == FUSE_WRITE);
509 }, Eq(true)),
510 _)
511 ).Times(0);
512 EXPECT_CALL(*m_mock, process(
513 ResultOf([=](auto in) {
514 return (in.header.opcode == FUSE_GETATTR &&
515 in.header.nodeid == ino);
516 }, Eq(true)),
517 _)
518 ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
519 SET_OUT_HEADER_LEN(out, attr);
520 out.body.attr.attr.mode = S_IFREG | 0644;
521 out.body.attr.attr_valid = UINT64_MAX;
522 out.body.attr.attr.size = bufsize;
523 out.body.attr.attr.uid = uid;
524 })));
525
526 /* Fill the data cache */
527 fd = open(FULLPATH, O_RDWR);
528 ASSERT_LE(0, fd) << strerror(errno);
529 ASSERT_EQ(bufsize, write(fd, CONTENTS, bufsize)) << strerror(errno);
530
531 /* Evict the attributes, but not data cache */
532 iia.mock = m_mock;
533 iia.ino = ino;
534 iia.off = -1;
535 iia.len = 0;
536 ASSERT_EQ(0, pthread_create(&th0, NULL, inval_inode, &iia))
537 << strerror(errno);
538 pthread_join(th0, &thr0_value);
539 EXPECT_EQ(0, (intptr_t)thr0_value);
540
541 /* cache attributes were been purged; this will trigger a new GETATTR */
542 ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
543 EXPECT_EQ(uid, sb.st_uid);
544 EXPECT_EQ(bufsize, sb.st_size);
545
546 leak(fd);
547 }
548