1/*-
2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
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 * $FreeBSD$
31 */
32
33extern "C" {
34#include <sys/file.h>
35#include <fcntl.h>
36}
37
38#include "mockfs.hh"
39#include "utils.hh"
40
41/* This flag value should probably be defined in fuse_kernel.h */
42#define OFFSET_MAX 0x7fffffffffffffffLL
43
44using namespace testing;
45
46/* For testing filesystems without posix locking support */
47class Fallback: public FuseTest {
48public:
49
50void expect_lookup(const char *relpath, uint64_t ino)
51{
52	FuseTest::expect_lookup(relpath, ino, S_IFREG | 0644, 0, 1);
53}
54
55};
56
57/* For testing filesystems with posix locking support */
58class Locks: public Fallback {
59	virtual void SetUp() {
60		m_init_flags = FUSE_POSIX_LOCKS;
61		Fallback::SetUp();
62	}
63};
64
65class Fcntl: public Locks {
66public:
67void expect_setlk(uint64_t ino, pid_t pid, uint64_t start, uint64_t end,
68	uint32_t type, int err)
69{
70	EXPECT_CALL(*m_mock, process(
71		ResultOf([=](auto in) {
72			return (in.header.opcode == FUSE_SETLK &&
73				in.header.nodeid == ino &&
74				in.body.setlk.fh == FH &&
75				in.body.setlk.owner == (uint32_t)pid &&
76				in.body.setlk.lk.start == start &&
77				in.body.setlk.lk.end == end &&
78				in.body.setlk.lk.type == type &&
79				in.body.setlk.lk.pid == (uint64_t)pid);
80		}, Eq(true)),
81		_)
82	).WillOnce(Invoke(ReturnErrno(err)));
83}
84void expect_setlkw(uint64_t ino, pid_t pid, uint64_t start, uint64_t end,
85	uint32_t type, int err)
86{
87	EXPECT_CALL(*m_mock, process(
88		ResultOf([=](auto in) {
89			return (in.header.opcode == FUSE_SETLKW &&
90				in.header.nodeid == ino &&
91				in.body.setlkw.fh == FH &&
92				in.body.setlkw.owner == (uint32_t)pid &&
93				in.body.setlkw.lk.start == start &&
94				in.body.setlkw.lk.end == end &&
95				in.body.setlkw.lk.type == type &&
96				in.body.setlkw.lk.pid == (uint64_t)pid);
97		}, Eq(true)),
98		_)
99	).WillOnce(Invoke(ReturnErrno(err)));
100}
101};
102
103class Flock: public Locks {
104public:
105void expect_setlk(uint64_t ino, uint32_t type, int err)
106{
107	EXPECT_CALL(*m_mock, process(
108		ResultOf([=](auto in) {
109			return (in.header.opcode == FUSE_SETLK &&
110				in.header.nodeid == ino &&
111				in.body.setlk.fh == FH &&
112				/*
113				 * The owner should be set to the address of
114				 * the vnode.  That's hard to verify.
115				 */
116				/* in.body.setlk.owner == ??? && */
117				in.body.setlk.lk.type == type);
118		}, Eq(true)),
119		_)
120	).WillOnce(Invoke(ReturnErrno(err)));
121}
122};
123
124class FlockFallback: public Fallback {};
125class GetlkFallback: public Fallback {};
126class Getlk: public Fcntl {};
127class SetlkFallback: public Fallback {};
128class Setlk: public Fcntl {};
129class SetlkwFallback: public Fallback {};
130class Setlkw: public Fcntl {};
131
132/*
133 * If the fuse filesystem does not support flock locks, then the kernel should
134 * fall back to local locks.
135 */
136TEST_F(FlockFallback, local)
137{
138	const char FULLPATH[] = "mountpoint/some_file.txt";
139	const char RELPATH[] = "some_file.txt";
140	uint64_t ino = 42;
141	int fd;
142
143	expect_lookup(RELPATH, ino);
144	expect_open(ino, 0, 1);
145
146	fd = open(FULLPATH, O_RDWR);
147	ASSERT_LE(0, fd) << strerror(errno);
148	ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
149	leak(fd);
150}
151
152/*
153 * Even if the fuse file system supports POSIX locks, we must implement flock
154 * locks locally until protocol 7.17.  Protocol 7.9 added partial buggy support
155 * but we won't implement that.
156 */
157TEST_F(Flock, local)
158{
159	const char FULLPATH[] = "mountpoint/some_file.txt";
160	const char RELPATH[] = "some_file.txt";
161	uint64_t ino = 42;
162	int fd;
163
164	expect_lookup(RELPATH, ino);
165	expect_open(ino, 0, 1);
166
167	fd = open(FULLPATH, O_RDWR);
168	ASSERT_LE(0, fd) << strerror(errno);
169	ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
170	leak(fd);
171}
172
173/* Set a new flock lock with FUSE_SETLK */
174/* TODO: enable after upgrading to protocol 7.17 */
175TEST_F(Flock, DISABLED_set)
176{
177	const char FULLPATH[] = "mountpoint/some_file.txt";
178	const char RELPATH[] = "some_file.txt";
179	uint64_t ino = 42;
180	int fd;
181
182	expect_lookup(RELPATH, ino);
183	expect_open(ino, 0, 1);
184	expect_setlk(ino, F_WRLCK, 0);
185
186	fd = open(FULLPATH, O_RDWR);
187	ASSERT_LE(0, fd) << strerror(errno);
188	ASSERT_EQ(0, flock(fd, LOCK_EX)) << strerror(errno);
189	leak(fd);
190}
191
192/* Fail to set a flock lock in non-blocking mode */
193/* TODO: enable after upgrading to protocol 7.17 */
194TEST_F(Flock, DISABLED_eagain)
195{
196	const char FULLPATH[] = "mountpoint/some_file.txt";
197	const char RELPATH[] = "some_file.txt";
198	uint64_t ino = 42;
199	int fd;
200
201	expect_lookup(RELPATH, ino);
202	expect_open(ino, 0, 1);
203	expect_setlk(ino, F_WRLCK, EAGAIN);
204
205	fd = open(FULLPATH, O_RDWR);
206	ASSERT_LE(0, fd) << strerror(errno);
207	ASSERT_NE(0, flock(fd, LOCK_EX | LOCK_NB));
208	ASSERT_EQ(EAGAIN, errno);
209	leak(fd);
210}
211
212/*
213 * If the fuse filesystem does not support posix file locks, then the kernel
214 * should fall back to local locks.
215 */
216TEST_F(GetlkFallback, local)
217{
218	const char FULLPATH[] = "mountpoint/some_file.txt";
219	const char RELPATH[] = "some_file.txt";
220	uint64_t ino = 42;
221	struct flock fl;
222	int fd;
223
224	expect_lookup(RELPATH, ino);
225	expect_open(ino, 0, 1);
226
227	fd = open(FULLPATH, O_RDWR);
228	ASSERT_LE(0, fd) << strerror(errno);
229	fl.l_start = 10;
230	fl.l_len = 1000;
231	fl.l_pid = getpid();
232	fl.l_type = F_RDLCK;
233	fl.l_whence = SEEK_SET;
234	fl.l_sysid = 0;
235	ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
236	leak(fd);
237}
238
239/*
240 * If the filesystem has no locks that fit the description, the filesystem
241 * should return F_UNLCK
242 */
243TEST_F(Getlk, no_locks)
244{
245	const char FULLPATH[] = "mountpoint/some_file.txt";
246	const char RELPATH[] = "some_file.txt";
247	uint64_t ino = 42;
248	struct flock fl;
249	int fd;
250	pid_t pid = 1234;
251
252	expect_lookup(RELPATH, ino);
253	expect_open(ino, 0, 1);
254	EXPECT_CALL(*m_mock, process(
255		ResultOf([=](auto in) {
256			return (in.header.opcode == FUSE_GETLK &&
257				in.header.nodeid == ino &&
258				in.body.getlk.fh == FH &&
259				in.body.getlk.owner == (uint32_t)pid &&
260				in.body.getlk.lk.start == 10 &&
261				in.body.getlk.lk.end == 1009 &&
262				in.body.getlk.lk.type == F_RDLCK &&
263				in.body.getlk.lk.pid == (uint64_t)pid);
264		}, Eq(true)),
265		_)
266	).WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
267		SET_OUT_HEADER_LEN(out, getlk);
268		out.body.getlk.lk = in.body.getlk.lk;
269		out.body.getlk.lk.type = F_UNLCK;
270	})));
271
272	fd = open(FULLPATH, O_RDWR);
273	ASSERT_LE(0, fd) << strerror(errno);
274	fl.l_start = 10;
275	fl.l_len = 1000;
276	fl.l_pid = pid;
277	fl.l_type = F_RDLCK;
278	fl.l_whence = SEEK_SET;
279	fl.l_sysid = 0;
280	ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
281	ASSERT_EQ(F_UNLCK, fl.l_type);
282	leak(fd);
283}
284
285/* A different pid does have a lock */
286TEST_F(Getlk, lock_exists)
287{
288	const char FULLPATH[] = "mountpoint/some_file.txt";
289	const char RELPATH[] = "some_file.txt";
290	uint64_t ino = 42;
291	struct flock fl;
292	int fd;
293	pid_t pid = 1234;
294	pid_t pid2 = 1235;
295
296	expect_lookup(RELPATH, ino);
297	expect_open(ino, 0, 1);
298	EXPECT_CALL(*m_mock, process(
299		ResultOf([=](auto in) {
300			return (in.header.opcode == FUSE_GETLK &&
301				in.header.nodeid == ino &&
302				in.body.getlk.fh == FH &&
303				in.body.getlk.owner == (uint32_t)pid &&
304				in.body.getlk.lk.start == 10 &&
305				in.body.getlk.lk.end == 1009 &&
306				in.body.getlk.lk.type == F_RDLCK &&
307				in.body.getlk.lk.pid == (uint64_t)pid);
308		}, Eq(true)),
309		_)
310	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
311		SET_OUT_HEADER_LEN(out, getlk);
312		out.body.getlk.lk.start = 100;
313		out.body.getlk.lk.end = 199;
314		out.body.getlk.lk.type = F_WRLCK;
315		out.body.getlk.lk.pid = (uint32_t)pid2;;
316	})));
317
318	fd = open(FULLPATH, O_RDWR);
319	ASSERT_LE(0, fd) << strerror(errno);
320	fl.l_start = 10;
321	fl.l_len = 1000;
322	fl.l_pid = pid;
323	fl.l_type = F_RDLCK;
324	fl.l_whence = SEEK_SET;
325	fl.l_sysid = 0;
326	ASSERT_NE(-1, fcntl(fd, F_GETLK, &fl)) << strerror(errno);
327	EXPECT_EQ(100, fl.l_start);
328	EXPECT_EQ(100, fl.l_len);
329	EXPECT_EQ(pid2, fl.l_pid);
330	EXPECT_EQ(F_WRLCK, fl.l_type);
331	EXPECT_EQ(SEEK_SET, fl.l_whence);
332	EXPECT_EQ(0, fl.l_sysid);
333	leak(fd);
334}
335
336/*
337 * If the fuse filesystem does not support posix file locks, then the kernel
338 * should fall back to local locks.
339 */
340TEST_F(SetlkFallback, local)
341{
342	const char FULLPATH[] = "mountpoint/some_file.txt";
343	const char RELPATH[] = "some_file.txt";
344	uint64_t ino = 42;
345	struct flock fl;
346	int fd;
347
348	expect_lookup(RELPATH, ino);
349	expect_open(ino, 0, 1);
350
351	fd = open(FULLPATH, O_RDWR);
352	ASSERT_LE(0, fd) << strerror(errno);
353	fl.l_start = 10;
354	fl.l_len = 1000;
355	fl.l_pid = getpid();
356	fl.l_type = F_RDLCK;
357	fl.l_whence = SEEK_SET;
358	fl.l_sysid = 0;
359	ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
360	leak(fd);
361}
362
363/* Clear a lock with FUSE_SETLK */
364TEST_F(Setlk, clear)
365{
366	const char FULLPATH[] = "mountpoint/some_file.txt";
367	const char RELPATH[] = "some_file.txt";
368	uint64_t ino = 42;
369	struct flock fl;
370	int fd;
371	pid_t pid = 1234;
372
373	expect_lookup(RELPATH, ino);
374	expect_open(ino, 0, 1);
375	expect_setlk(ino, pid, 10, 1009, F_UNLCK, 0);
376
377	fd = open(FULLPATH, O_RDWR);
378	ASSERT_LE(0, fd) << strerror(errno);
379	fl.l_start = 10;
380	fl.l_len = 1000;
381	fl.l_pid = pid;
382	fl.l_type = F_UNLCK;
383	fl.l_whence = SEEK_SET;
384	fl.l_sysid = 0;
385	ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
386	leak(fd);
387}
388
389/* Set a new lock with FUSE_SETLK */
390TEST_F(Setlk, set)
391{
392	const char FULLPATH[] = "mountpoint/some_file.txt";
393	const char RELPATH[] = "some_file.txt";
394	uint64_t ino = 42;
395	struct flock fl;
396	int fd;
397	pid_t pid = 1234;
398
399	expect_lookup(RELPATH, ino);
400	expect_open(ino, 0, 1);
401	expect_setlk(ino, pid, 10, 1009, F_RDLCK, 0);
402
403	fd = open(FULLPATH, O_RDWR);
404	ASSERT_LE(0, fd) << strerror(errno);
405	fl.l_start = 10;
406	fl.l_len = 1000;
407	fl.l_pid = pid;
408	fl.l_type = F_RDLCK;
409	fl.l_whence = SEEK_SET;
410	fl.l_sysid = 0;
411	ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
412	leak(fd);
413}
414
415/* l_len = 0 is a flag value that means to lock until EOF */
416TEST_F(Setlk, set_eof)
417{
418	const char FULLPATH[] = "mountpoint/some_file.txt";
419	const char RELPATH[] = "some_file.txt";
420	uint64_t ino = 42;
421	struct flock fl;
422	int fd;
423	pid_t pid = 1234;
424
425	expect_lookup(RELPATH, ino);
426	expect_open(ino, 0, 1);
427	expect_setlk(ino, pid, 10, OFFSET_MAX, F_RDLCK, 0);
428
429	fd = open(FULLPATH, O_RDWR);
430	ASSERT_LE(0, fd) << strerror(errno);
431	fl.l_start = 10;
432	fl.l_len = 0;
433	fl.l_pid = pid;
434	fl.l_type = F_RDLCK;
435	fl.l_whence = SEEK_SET;
436	fl.l_sysid = 0;
437	ASSERT_NE(-1, fcntl(fd, F_SETLK, &fl)) << strerror(errno);
438	leak(fd);
439}
440
441/* Fail to set a new lock with FUSE_SETLK due to a conflict */
442TEST_F(Setlk, eagain)
443{
444	const char FULLPATH[] = "mountpoint/some_file.txt";
445	const char RELPATH[] = "some_file.txt";
446	uint64_t ino = 42;
447	struct flock fl;
448	int fd;
449	pid_t pid = 1234;
450
451	expect_lookup(RELPATH, ino);
452	expect_open(ino, 0, 1);
453	expect_setlk(ino, pid, 10, 1009, F_RDLCK, EAGAIN);
454
455	fd = open(FULLPATH, O_RDWR);
456	ASSERT_LE(0, fd) << strerror(errno);
457	fl.l_start = 10;
458	fl.l_len = 1000;
459	fl.l_pid = pid;
460	fl.l_type = F_RDLCK;
461	fl.l_whence = SEEK_SET;
462	fl.l_sysid = 0;
463	ASSERT_EQ(-1, fcntl(fd, F_SETLK, &fl));
464	ASSERT_EQ(EAGAIN, errno);
465	leak(fd);
466}
467
468/*
469 * If the fuse filesystem does not support posix file locks, then the kernel
470 * should fall back to local locks.
471 */
472TEST_F(SetlkwFallback, local)
473{
474	const char FULLPATH[] = "mountpoint/some_file.txt";
475	const char RELPATH[] = "some_file.txt";
476	uint64_t ino = 42;
477	struct flock fl;
478	int fd;
479
480	expect_lookup(RELPATH, ino);
481	expect_open(ino, 0, 1);
482
483	fd = open(FULLPATH, O_RDWR);
484	ASSERT_LE(0, fd) << strerror(errno);
485	fl.l_start = 10;
486	fl.l_len = 1000;
487	fl.l_pid = getpid();
488	fl.l_type = F_RDLCK;
489	fl.l_whence = SEEK_SET;
490	fl.l_sysid = 0;
491	ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
492	leak(fd);
493}
494
495/*
496 * Set a new lock with FUSE_SETLK.  If the lock is not available, then the
497 * command should block.  But to the kernel, that's the same as just being
498 * slow, so we don't need a separate test method
499 */
500TEST_F(Setlkw, set)
501{
502	const char FULLPATH[] = "mountpoint/some_file.txt";
503	const char RELPATH[] = "some_file.txt";
504	uint64_t ino = 42;
505	struct flock fl;
506	int fd;
507	pid_t pid = 1234;
508
509	expect_lookup(RELPATH, ino);
510	expect_open(ino, 0, 1);
511	expect_setlkw(ino, pid, 10, 1009, F_RDLCK, 0);
512
513	fd = open(FULLPATH, O_RDWR);
514	ASSERT_LE(0, fd) << strerror(errno);
515	fl.l_start = 10;
516	fl.l_len = 1000;
517	fl.l_pid = pid;
518	fl.l_type = F_RDLCK;
519	fl.l_whence = SEEK_SET;
520	fl.l_sysid = 0;
521	ASSERT_NE(-1, fcntl(fd, F_SETLKW, &fl)) << strerror(errno);
522	leak(fd);
523}
524