Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/tests/sys/fs/fusefs/setattr.cc
39537 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
#include <sys/resource.h>
34
#include <sys/stat.h>
35
#include <sys/time.h>
36
37
#include <fcntl.h>
38
#include <semaphore.h>
39
#include <signal.h>
40
}
41
42
#include "mockfs.hh"
43
#include "utils.hh"
44
45
using namespace testing;
46
47
class Setattr : public FuseTest {
48
public:
49
static sig_atomic_t s_sigxfsz;
50
};
51
52
class RofsSetattr: public Setattr {
53
public:
54
virtual void SetUp() {
55
s_sigxfsz = 0;
56
m_ro = true;
57
Setattr::SetUp();
58
}
59
};
60
61
class Setattr_7_8: public Setattr {
62
public:
63
virtual void SetUp() {
64
m_kernel_minor_version = 8;
65
Setattr::SetUp();
66
}
67
};
68
69
70
sig_atomic_t Setattr::s_sigxfsz = 0;
71
72
void sigxfsz_handler(int __unused sig) {
73
Setattr::s_sigxfsz = 1;
74
}
75
76
/*
77
* If setattr returns a non-zero cache timeout, then subsequent VOP_GETATTRs
78
* should use the cached attributes, rather than query the daemon
79
*/
80
TEST_F(Setattr, attr_cache)
81
{
82
const char FULLPATH[] = "mountpoint/some_file.txt";
83
const char RELPATH[] = "some_file.txt";
84
const uint64_t ino = 42;
85
struct stat sb;
86
const mode_t newmode = 0644;
87
88
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
89
.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
90
SET_OUT_HEADER_LEN(out, entry);
91
out.body.entry.attr.mode = S_IFREG | 0644;
92
out.body.entry.nodeid = ino;
93
out.body.entry.entry_valid = UINT64_MAX;
94
})));
95
96
EXPECT_CALL(*m_mock, process(
97
ResultOf([](auto in) {
98
return (in.header.opcode == FUSE_SETATTR &&
99
in.header.nodeid == ino);
100
}, Eq(true)),
101
_)
102
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
103
SET_OUT_HEADER_LEN(out, attr);
104
out.body.attr.attr.ino = ino; // Must match nodeid
105
out.body.attr.attr.mode = S_IFREG | newmode;
106
out.body.attr.attr_valid = UINT64_MAX;
107
})));
108
EXPECT_CALL(*m_mock, process(
109
ResultOf([](auto in) {
110
return (in.header.opcode == FUSE_GETATTR);
111
}, Eq(true)),
112
_)
113
).Times(0);
114
115
/* Set an attribute with SETATTR */
116
ASSERT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
117
118
/* The stat(2) should use cached attributes */
119
ASSERT_EQ(0, stat(FULLPATH, &sb));
120
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
121
}
122
123
/* Change the mode of a file */
124
TEST_F(Setattr, chmod)
125
{
126
const char FULLPATH[] = "mountpoint/some_file.txt";
127
const char RELPATH[] = "some_file.txt";
128
const uint64_t ino = 42;
129
const mode_t oldmode = 0755;
130
const mode_t newmode = 0644;
131
132
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
133
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
134
SET_OUT_HEADER_LEN(out, entry);
135
out.body.entry.attr.mode = S_IFREG | oldmode;
136
out.body.entry.nodeid = ino;
137
})));
138
139
EXPECT_CALL(*m_mock, process(
140
ResultOf([](auto in) {
141
uint32_t valid = FATTR_MODE;
142
return (in.header.opcode == FUSE_SETATTR &&
143
in.header.nodeid == ino &&
144
in.body.setattr.valid == valid &&
145
in.body.setattr.mode == newmode);
146
}, Eq(true)),
147
_)
148
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
149
SET_OUT_HEADER_LEN(out, attr);
150
out.body.attr.attr.ino = ino; // Must match nodeid
151
out.body.attr.attr.mode = S_IFREG | newmode;
152
})));
153
EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
154
}
155
156
/*
157
* Chmod a multiply-linked file with cached attributes. Check that both files'
158
* attributes have changed.
159
*/
160
TEST_F(Setattr, chmod_multiply_linked)
161
{
162
const char FULLPATH0[] = "mountpoint/some_file.txt";
163
const char RELPATH0[] = "some_file.txt";
164
const char FULLPATH1[] = "mountpoint/other_file.txt";
165
const char RELPATH1[] = "other_file.txt";
166
struct stat sb;
167
const uint64_t ino = 42;
168
const mode_t oldmode = 0777;
169
const mode_t newmode = 0666;
170
171
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH0)
172
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
173
SET_OUT_HEADER_LEN(out, entry);
174
out.body.entry.attr.mode = S_IFREG | oldmode;
175
out.body.entry.nodeid = ino;
176
out.body.entry.attr.nlink = 2;
177
out.body.entry.attr_valid = UINT64_MAX;
178
out.body.entry.entry_valid = UINT64_MAX;
179
})));
180
181
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH1)
182
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
183
SET_OUT_HEADER_LEN(out, entry);
184
out.body.entry.attr.mode = S_IFREG | oldmode;
185
out.body.entry.nodeid = ino;
186
out.body.entry.attr.nlink = 2;
187
out.body.entry.attr_valid = UINT64_MAX;
188
out.body.entry.entry_valid = UINT64_MAX;
189
})));
190
191
EXPECT_CALL(*m_mock, process(
192
ResultOf([](auto in) {
193
uint32_t valid = FATTR_MODE;
194
return (in.header.opcode == FUSE_SETATTR &&
195
in.header.nodeid == ino &&
196
in.body.setattr.valid == valid &&
197
in.body.setattr.mode == newmode);
198
}, Eq(true)),
199
_)
200
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
201
SET_OUT_HEADER_LEN(out, attr);
202
out.body.attr.attr.ino = ino;
203
out.body.attr.attr.mode = S_IFREG | newmode;
204
out.body.attr.attr.nlink = 2;
205
out.body.attr.attr_valid = UINT64_MAX;
206
})));
207
208
/* For a lookup of the 2nd file to get it into the cache*/
209
ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
210
EXPECT_EQ(S_IFREG | oldmode, sb.st_mode);
211
212
ASSERT_EQ(0, chmod(FULLPATH0, newmode)) << strerror(errno);
213
ASSERT_EQ(0, stat(FULLPATH0, &sb)) << strerror(errno);
214
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
215
ASSERT_EQ(0, stat(FULLPATH1, &sb)) << strerror(errno);
216
EXPECT_EQ(S_IFREG | newmode, sb.st_mode);
217
}
218
219
220
/* Change the owner and group of a file */
221
TEST_F(Setattr, chown)
222
{
223
const char FULLPATH[] = "mountpoint/some_file.txt";
224
const char RELPATH[] = "some_file.txt";
225
const uint64_t ino = 42;
226
const gid_t oldgroup = 66;
227
const gid_t newgroup = 99;
228
const uid_t olduser = 33;
229
const uid_t newuser = 44;
230
231
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
232
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
233
SET_OUT_HEADER_LEN(out, entry);
234
out.body.entry.attr.mode = S_IFREG | 0644;
235
out.body.entry.nodeid = ino;
236
out.body.entry.attr.gid = oldgroup;
237
out.body.entry.attr.uid = olduser;
238
})));
239
240
EXPECT_CALL(*m_mock, process(
241
ResultOf([](auto in) {
242
uint32_t valid = FATTR_GID | FATTR_UID;
243
return (in.header.opcode == FUSE_SETATTR &&
244
in.header.nodeid == ino &&
245
in.body.setattr.valid == valid &&
246
in.body.setattr.uid == newuser &&
247
in.body.setattr.gid == newgroup);
248
}, Eq(true)),
249
_)
250
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
251
SET_OUT_HEADER_LEN(out, attr);
252
out.body.attr.attr.ino = ino; // Must match nodeid
253
out.body.attr.attr.mode = S_IFREG | 0644;
254
out.body.attr.attr.uid = newuser;
255
out.body.attr.attr.gid = newgroup;
256
})));
257
EXPECT_EQ(0, chown(FULLPATH, newuser, newgroup)) << strerror(errno);
258
}
259
260
261
262
/*
263
* FUSE daemons are allowed to check permissions however they like. If the
264
* daemon returns EPERM, even if the file permissions "should" grant access,
265
* then fuse(4) should return EPERM too.
266
*/
267
TEST_F(Setattr, eperm)
268
{
269
const char FULLPATH[] = "mountpoint/some_file.txt";
270
const char RELPATH[] = "some_file.txt";
271
const uint64_t ino = 42;
272
273
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
274
.WillOnce(Invoke(ReturnImmediate([=](auto in, auto& out) {
275
SET_OUT_HEADER_LEN(out, entry);
276
out.body.entry.attr.mode = S_IFREG | 0777;
277
out.body.entry.nodeid = ino;
278
out.body.entry.attr.uid = in.header.uid;
279
out.body.entry.attr.gid = in.header.gid;
280
})));
281
282
EXPECT_CALL(*m_mock, process(
283
ResultOf([](auto in) {
284
return (in.header.opcode == FUSE_SETATTR &&
285
in.header.nodeid == ino);
286
}, Eq(true)),
287
_)
288
).WillOnce(Invoke(ReturnErrno(EPERM)));
289
EXPECT_NE(0, truncate(FULLPATH, 10));
290
EXPECT_EQ(EPERM, errno);
291
}
292
293
/* Change the mode of an open file, by its file descriptor */
294
TEST_F(Setattr, fchmod)
295
{
296
const char FULLPATH[] = "mountpoint/some_file.txt";
297
const char RELPATH[] = "some_file.txt";
298
uint64_t ino = 42;
299
int fd;
300
const mode_t oldmode = 0755;
301
const mode_t newmode = 0644;
302
303
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
304
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
305
SET_OUT_HEADER_LEN(out, entry);
306
out.body.entry.attr.mode = S_IFREG | oldmode;
307
out.body.entry.nodeid = ino;
308
out.body.entry.attr_valid = UINT64_MAX;
309
})));
310
311
EXPECT_CALL(*m_mock, process(
312
ResultOf([=](auto in) {
313
return (in.header.opcode == FUSE_OPEN &&
314
in.header.nodeid == ino);
315
}, Eq(true)),
316
_)
317
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
318
out.header.len = sizeof(out.header);
319
SET_OUT_HEADER_LEN(out, open);
320
})));
321
322
EXPECT_CALL(*m_mock, process(
323
ResultOf([=](auto in) {
324
uint32_t valid = FATTR_MODE;
325
return (in.header.opcode == FUSE_SETATTR &&
326
in.header.nodeid == ino &&
327
in.body.setattr.valid == valid &&
328
in.body.setattr.mode == newmode);
329
}, Eq(true)),
330
_)
331
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
332
SET_OUT_HEADER_LEN(out, attr);
333
out.body.attr.attr.ino = ino; // Must match nodeid
334
out.body.attr.attr.mode = S_IFREG | newmode;
335
})));
336
337
fd = open(FULLPATH, O_RDONLY);
338
ASSERT_LE(0, fd) << strerror(errno);
339
ASSERT_EQ(0, fchmod(fd, newmode)) << strerror(errno);
340
leak(fd);
341
}
342
343
/* Change the size of an open file, by its file descriptor */
344
TEST_F(Setattr, ftruncate)
345
{
346
const char FULLPATH[] = "mountpoint/some_file.txt";
347
const char RELPATH[] = "some_file.txt";
348
uint64_t ino = 42;
349
int fd;
350
uint64_t fh = 0xdeadbeef1a7ebabe;
351
const off_t oldsize = 99;
352
const off_t newsize = 12345;
353
354
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
355
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
356
SET_OUT_HEADER_LEN(out, entry);
357
out.body.entry.attr.mode = S_IFREG | 0755;
358
out.body.entry.nodeid = ino;
359
out.body.entry.attr_valid = UINT64_MAX;
360
out.body.entry.attr.size = oldsize;
361
})));
362
363
EXPECT_CALL(*m_mock, process(
364
ResultOf([=](auto in) {
365
return (in.header.opcode == FUSE_OPEN &&
366
in.header.nodeid == ino);
367
}, Eq(true)),
368
_)
369
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
370
out.header.len = sizeof(out.header);
371
SET_OUT_HEADER_LEN(out, open);
372
out.body.open.fh = fh;
373
})));
374
375
EXPECT_CALL(*m_mock, process(
376
ResultOf([=](auto in) {
377
uint32_t valid = FATTR_SIZE | FATTR_FH;
378
return (in.header.opcode == FUSE_SETATTR &&
379
in.header.nodeid == ino &&
380
in.body.setattr.valid == valid &&
381
in.body.setattr.fh == fh);
382
}, Eq(true)),
383
_)
384
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
385
SET_OUT_HEADER_LEN(out, attr);
386
out.body.attr.attr.ino = ino; // Must match nodeid
387
out.body.attr.attr.mode = S_IFREG | 0755;
388
out.body.attr.attr.size = newsize;
389
})));
390
391
fd = open(FULLPATH, O_RDWR);
392
ASSERT_LE(0, fd) << strerror(errno);
393
ASSERT_EQ(0, ftruncate(fd, newsize)) << strerror(errno);
394
leak(fd);
395
}
396
397
/* Change the size of the file */
398
TEST_F(Setattr, truncate) {
399
const char FULLPATH[] = "mountpoint/some_file.txt";
400
const char RELPATH[] = "some_file.txt";
401
const uint64_t ino = 42;
402
const uint64_t oldsize = 100'000'000;
403
const uint64_t newsize = 20'000'000;
404
405
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
406
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
407
SET_OUT_HEADER_LEN(out, entry);
408
out.body.entry.attr.mode = S_IFREG | 0644;
409
out.body.entry.nodeid = ino;
410
out.body.entry.attr.size = oldsize;
411
})));
412
413
EXPECT_CALL(*m_mock, process(
414
ResultOf([](auto in) {
415
uint32_t valid = FATTR_SIZE;
416
return (in.header.opcode == FUSE_SETATTR &&
417
in.header.nodeid == ino &&
418
in.body.setattr.valid == valid &&
419
in.body.setattr.size == newsize);
420
}, Eq(true)),
421
_)
422
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
423
SET_OUT_HEADER_LEN(out, attr);
424
out.body.attr.attr.ino = ino; // Must match nodeid
425
out.body.attr.attr.mode = S_IFREG | 0644;
426
out.body.attr.attr.size = newsize;
427
})));
428
EXPECT_EQ(0, truncate(FULLPATH, newsize)) << strerror(errno);
429
}
430
431
/*
432
* Truncating a file should discard cached data past the truncation point.
433
* This is a regression test for bug 233783.
434
*
435
* There are two distinct failure modes. The first one is a failure to zero
436
* the portion of the file's final buffer past EOF. It can be reproduced by
437
* fsx -WR -P /tmp -S10 fsx.bin
438
*
439
* The second is a failure to drop buffers beyond that. It can be reproduced by
440
* fsx -WR -P /tmp -S18 -n fsx.bin
441
* Also reproducible in sh with:
442
* $> /path/to/libfuse/build/example/passthrough -d /tmp/mnt
443
* $> cd /tmp/mnt/tmp
444
* $> dd if=/dev/random of=randfile bs=1k count=192
445
* $> truncate -s 1k randfile && truncate -s 192k randfile
446
* $> xxd randfile | less # xxd will wrongly show random data at offset 0x8000
447
*/
448
TEST_F(Setattr, truncate_discards_cached_data) {
449
const char FULLPATH[] = "mountpoint/some_file.txt";
450
const char RELPATH[] = "some_file.txt";
451
char *w0buf, *r0buf, *r1buf, *expected;
452
off_t w0_offset = 0;
453
size_t w0_size = 0x30000;
454
off_t r0_offset = 0;
455
off_t r0_size = w0_size;
456
size_t trunc0_size = 0x400;
457
size_t trunc1_size = w0_size;
458
off_t r1_offset = trunc0_size;
459
off_t r1_size = w0_size - trunc0_size;
460
size_t cur_size = 0;
461
const uint64_t ino = 42;
462
mode_t mode = S_IFREG | 0644;
463
int fd, r;
464
bool should_have_data = false;
465
466
w0buf = new char[w0_size];
467
memset(w0buf, 'X', w0_size);
468
469
r0buf = new char[r0_size];
470
r1buf = new char[r1_size];
471
472
expected = new char[r1_size]();
473
474
expect_lookup(RELPATH, ino, mode, 0, 1);
475
expect_open(ino, O_RDWR, 1);
476
EXPECT_CALL(*m_mock, process(
477
ResultOf([=](auto in) {
478
return (in.header.opcode == FUSE_GETATTR &&
479
in.header.nodeid == ino);
480
}, Eq(true)),
481
_)
482
).WillRepeatedly(Invoke(ReturnImmediate([&](auto i __unused, auto& out) {
483
SET_OUT_HEADER_LEN(out, attr);
484
out.body.attr.attr.ino = ino;
485
out.body.attr.attr.mode = mode;
486
out.body.attr.attr.size = cur_size;
487
})));
488
EXPECT_CALL(*m_mock, process(
489
ResultOf([=](auto in) {
490
return (in.header.opcode == FUSE_WRITE);
491
}, Eq(true)),
492
_)
493
).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
494
SET_OUT_HEADER_LEN(out, write);
495
out.body.attr.attr.ino = ino;
496
out.body.write.size = in.body.write.size;
497
cur_size = std::max(static_cast<uint64_t>(cur_size),
498
in.body.write.size + in.body.write.offset);
499
})));
500
501
EXPECT_CALL(*m_mock, process(
502
ResultOf([=](auto in) {
503
return (in.header.opcode == FUSE_SETATTR &&
504
in.header.nodeid == ino &&
505
(in.body.setattr.valid & FATTR_SIZE));
506
}, Eq(true)),
507
_)
508
).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
509
auto trunc_size = in.body.setattr.size;
510
SET_OUT_HEADER_LEN(out, attr);
511
out.body.attr.attr.ino = ino;
512
out.body.attr.attr.mode = mode;
513
out.body.attr.attr.size = trunc_size;
514
cur_size = trunc_size;
515
})));
516
517
EXPECT_CALL(*m_mock, process(
518
ResultOf([=](auto in) {
519
return (in.header.opcode == FUSE_READ);
520
}, Eq(true)),
521
_)
522
).WillRepeatedly(Invoke(ReturnImmediate([&](auto in, auto& out) {
523
auto osize = std::min(
524
static_cast<uint64_t>(cur_size) - in.body.read.offset,
525
static_cast<uint64_t>(in.body.read.size));
526
assert(osize <= sizeof(out.body.bytes));
527
out.header.len = sizeof(struct fuse_out_header) + osize;
528
if (should_have_data)
529
memset(out.body.bytes, 'X', osize);
530
else
531
bzero(out.body.bytes, osize);
532
})));
533
534
fd = open(FULLPATH, O_RDWR, 0644);
535
ASSERT_LE(0, fd) << strerror(errno);
536
537
/* Fill the file with Xs */
538
ASSERT_EQ(static_cast<ssize_t>(w0_size),
539
pwrite(fd, w0buf, w0_size, w0_offset));
540
should_have_data = true;
541
/* Fill the cache */
542
ASSERT_EQ(static_cast<ssize_t>(r0_size),
543
pread(fd, r0buf, r0_size, r0_offset));
544
/* 1st truncate should discard cached data */
545
EXPECT_EQ(0, ftruncate(fd, trunc0_size)) << strerror(errno);
546
should_have_data = false;
547
/* 2nd truncate extends file into previously cached data */
548
EXPECT_EQ(0, ftruncate(fd, trunc1_size)) << strerror(errno);
549
/* Read should return all zeros */
550
ASSERT_EQ(static_cast<ssize_t>(r1_size),
551
pread(fd, r1buf, r1_size, r1_offset));
552
553
r = memcmp(expected, r1buf, r1_size);
554
ASSERT_EQ(0, r);
555
556
delete[] expected;
557
delete[] r1buf;
558
delete[] r0buf;
559
delete[] w0buf;
560
561
leak(fd);
562
}
563
564
/* truncate should fail if it would cause the file to exceed RLIMIT_FSIZE */
565
TEST_F(Setattr, truncate_rlimit_rsize)
566
{
567
const char FULLPATH[] = "mountpoint/some_file.txt";
568
const char RELPATH[] = "some_file.txt";
569
struct rlimit rl;
570
const uint64_t ino = 42;
571
const uint64_t oldsize = 0;
572
const uint64_t newsize = 100'000'000;
573
574
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
575
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
576
SET_OUT_HEADER_LEN(out, entry);
577
out.body.entry.attr.mode = S_IFREG | 0644;
578
out.body.entry.nodeid = ino;
579
out.body.entry.attr.size = oldsize;
580
})));
581
582
rl.rlim_cur = newsize / 2;
583
rl.rlim_max = 10 * newsize;
584
ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
585
ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
586
587
EXPECT_EQ(-1, truncate(FULLPATH, newsize));
588
EXPECT_EQ(EFBIG, errno);
589
EXPECT_EQ(1, s_sigxfsz);
590
}
591
592
/* Change a file's timestamps */
593
TEST_F(Setattr, utimensat) {
594
const char FULLPATH[] = "mountpoint/some_file.txt";
595
const char RELPATH[] = "some_file.txt";
596
const uint64_t ino = 42;
597
const timespec oldtimes[2] = {
598
{.tv_sec = 1, .tv_nsec = 2},
599
{.tv_sec = 3, .tv_nsec = 4},
600
};
601
const timespec newtimes[2] = {
602
{.tv_sec = 5, .tv_nsec = 6},
603
{.tv_sec = 7, .tv_nsec = 8},
604
};
605
606
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
607
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
608
SET_OUT_HEADER_LEN(out, entry);
609
out.body.entry.attr.mode = S_IFREG | 0644;
610
out.body.entry.nodeid = ino;
611
out.body.entry.attr_valid = UINT64_MAX;
612
out.body.entry.attr.atime = oldtimes[0].tv_sec;
613
out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
614
out.body.entry.attr.mtime = oldtimes[1].tv_sec;
615
out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
616
})));
617
618
EXPECT_CALL(*m_mock, process(
619
ResultOf([=](auto in) {
620
uint32_t valid = FATTR_ATIME | FATTR_MTIME;
621
return (in.header.opcode == FUSE_SETATTR &&
622
in.header.nodeid == ino &&
623
in.body.setattr.valid == valid &&
624
(time_t)in.body.setattr.atime ==
625
newtimes[0].tv_sec &&
626
(long)in.body.setattr.atimensec ==
627
newtimes[0].tv_nsec &&
628
(time_t)in.body.setattr.mtime ==
629
newtimes[1].tv_sec &&
630
(long)in.body.setattr.mtimensec ==
631
newtimes[1].tv_nsec);
632
}, Eq(true)),
633
_)
634
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
635
SET_OUT_HEADER_LEN(out, attr);
636
out.body.attr.attr.ino = ino; // Must match nodeid
637
out.body.attr.attr.mode = S_IFREG | 0644;
638
out.body.attr.attr.atime = newtimes[0].tv_sec;
639
out.body.attr.attr.atimensec = newtimes[0].tv_nsec;
640
out.body.attr.attr.mtime = newtimes[1].tv_sec;
641
out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
642
})));
643
EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
644
<< strerror(errno);
645
}
646
647
/* Change a file mtime but not its atime */
648
TEST_F(Setattr, utimensat_mtime_only) {
649
const char FULLPATH[] = "mountpoint/some_file.txt";
650
const char RELPATH[] = "some_file.txt";
651
const uint64_t ino = 42;
652
const timespec oldtimes[2] = {
653
{.tv_sec = 1, .tv_nsec = 2},
654
{.tv_sec = 3, .tv_nsec = 4},
655
};
656
const timespec newtimes[2] = {
657
{.tv_sec = 5, .tv_nsec = UTIME_OMIT},
658
{.tv_sec = 7, .tv_nsec = 8},
659
};
660
661
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
662
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
663
SET_OUT_HEADER_LEN(out, entry);
664
out.body.entry.attr.mode = S_IFREG | 0644;
665
out.body.entry.nodeid = ino;
666
out.body.entry.attr_valid = UINT64_MAX;
667
out.body.entry.attr.atime = oldtimes[0].tv_sec;
668
out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
669
out.body.entry.attr.mtime = oldtimes[1].tv_sec;
670
out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
671
})));
672
673
EXPECT_CALL(*m_mock, process(
674
ResultOf([=](auto in) {
675
uint32_t valid = FATTR_MTIME;
676
return (in.header.opcode == FUSE_SETATTR &&
677
in.header.nodeid == ino &&
678
in.body.setattr.valid == valid &&
679
(time_t)in.body.setattr.mtime ==
680
newtimes[1].tv_sec &&
681
(long)in.body.setattr.mtimensec ==
682
newtimes[1].tv_nsec);
683
}, Eq(true)),
684
_)
685
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
686
SET_OUT_HEADER_LEN(out, attr);
687
out.body.attr.attr.ino = ino; // Must match nodeid
688
out.body.attr.attr.mode = S_IFREG | 0644;
689
out.body.attr.attr.atime = oldtimes[0].tv_sec;
690
out.body.attr.attr.atimensec = oldtimes[0].tv_nsec;
691
out.body.attr.attr.mtime = newtimes[1].tv_sec;
692
out.body.attr.attr.mtimensec = newtimes[1].tv_nsec;
693
})));
694
EXPECT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
695
<< strerror(errno);
696
}
697
698
/*
699
* Set a file's mtime and atime to now
700
*
701
* The design of FreeBSD's VFS does not allow fusefs to set just one of atime
702
* or mtime to UTIME_NOW; it's both or neither.
703
*/
704
TEST_F(Setattr, utimensat_utime_now) {
705
const char FULLPATH[] = "mountpoint/some_file.txt";
706
const char RELPATH[] = "some_file.txt";
707
const uint64_t ino = 42;
708
const timespec oldtimes[2] = {
709
{.tv_sec = 1, .tv_nsec = 2},
710
{.tv_sec = 3, .tv_nsec = 4},
711
};
712
const timespec newtimes[2] = {
713
{.tv_sec = 0, .tv_nsec = UTIME_NOW},
714
{.tv_sec = 0, .tv_nsec = UTIME_NOW},
715
};
716
/* "now" is whatever the server says it is */
717
const timespec now[2] = {
718
{.tv_sec = 5, .tv_nsec = 7},
719
{.tv_sec = 6, .tv_nsec = 8},
720
};
721
struct stat sb;
722
723
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
724
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
725
SET_OUT_HEADER_LEN(out, entry);
726
out.body.entry.attr.mode = S_IFREG | 0644;
727
out.body.entry.nodeid = ino;
728
out.body.entry.attr_valid = UINT64_MAX;
729
out.body.entry.entry_valid = UINT64_MAX;
730
out.body.entry.attr.atime = oldtimes[0].tv_sec;
731
out.body.entry.attr.atimensec = oldtimes[0].tv_nsec;
732
out.body.entry.attr.mtime = oldtimes[1].tv_sec;
733
out.body.entry.attr.mtimensec = oldtimes[1].tv_nsec;
734
})));
735
736
EXPECT_CALL(*m_mock, process(
737
ResultOf([=](auto in) {
738
uint32_t valid = FATTR_ATIME | FATTR_ATIME_NOW |
739
FATTR_MTIME | FATTR_MTIME_NOW;
740
return (in.header.opcode == FUSE_SETATTR &&
741
in.header.nodeid == ino &&
742
in.body.setattr.valid == valid);
743
}, Eq(true)),
744
_)
745
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
746
SET_OUT_HEADER_LEN(out, attr);
747
out.body.attr.attr.ino = ino; // Must match nodeid
748
out.body.attr.attr.mode = S_IFREG | 0644;
749
out.body.attr.attr.atime = now[0].tv_sec;
750
out.body.attr.attr.atimensec = now[0].tv_nsec;
751
out.body.attr.attr.mtime = now[1].tv_sec;
752
out.body.attr.attr.mtimensec = now[1].tv_nsec;
753
out.body.attr.attr_valid = UINT64_MAX;
754
})));
755
ASSERT_EQ(0, utimensat(AT_FDCWD, FULLPATH, &newtimes[0], 0))
756
<< strerror(errno);
757
ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);
758
EXPECT_EQ(now[0].tv_sec, sb.st_atim.tv_sec);
759
EXPECT_EQ(now[0].tv_nsec, sb.st_atim.tv_nsec);
760
EXPECT_EQ(now[1].tv_sec, sb.st_mtim.tv_sec);
761
EXPECT_EQ(now[1].tv_nsec, sb.st_mtim.tv_nsec);
762
}
763
764
/*
765
* FUSE_SETATTR returns a different file type, even though the entry cache
766
* hasn't expired. This is a server bug! It probably means that the server
767
* removed the file and recreated it with the same inode but a different vtyp.
768
* The best thing fusefs can do is return ENOENT to the caller. After all, the
769
* entry must not have existed recently.
770
*/
771
TEST_F(Setattr, vtyp_conflict)
772
{
773
const char FULLPATH[] = "mountpoint/some_file.txt";
774
const char RELPATH[] = "some_file.txt";
775
const uint64_t ino = 42;
776
uid_t newuser = 12345;
777
sem_t sem;
778
779
ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);
780
781
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
782
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
783
SET_OUT_HEADER_LEN(out, entry);
784
out.body.entry.attr.mode = S_IFREG | 0777;
785
out.body.entry.nodeid = ino;
786
out.body.entry.entry_valid = UINT64_MAX;
787
})));
788
789
EXPECT_CALL(*m_mock, process(
790
ResultOf([](auto in) {
791
return (in.header.opcode == FUSE_SETATTR &&
792
in.header.nodeid == ino);
793
}, Eq(true)),
794
_)
795
).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
796
SET_OUT_HEADER_LEN(out, attr);
797
out.body.attr.attr.ino = ino;
798
out.body.attr.attr.mode = S_IFDIR | 0777; // Changed!
799
out.body.attr.attr.uid = newuser;
800
})));
801
// We should reclaim stale vnodes
802
expect_forget(ino, 1, &sem);
803
804
EXPECT_NE(0, chown(FULLPATH, newuser, -1));
805
EXPECT_EQ(ENOENT, errno);
806
807
sem_wait(&sem);
808
sem_destroy(&sem);
809
}
810
811
/* On a read-only mount, no attributes may be changed */
812
TEST_F(RofsSetattr, erofs)
813
{
814
const char FULLPATH[] = "mountpoint/some_file.txt";
815
const char RELPATH[] = "some_file.txt";
816
const uint64_t ino = 42;
817
const mode_t oldmode = 0755;
818
const mode_t newmode = 0644;
819
820
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
821
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
822
SET_OUT_HEADER_LEN(out, entry);
823
out.body.entry.attr.mode = S_IFREG | oldmode;
824
out.body.entry.nodeid = ino;
825
})));
826
827
ASSERT_EQ(-1, chmod(FULLPATH, newmode));
828
ASSERT_EQ(EROFS, errno);
829
}
830
831
/* Change the mode of a file */
832
TEST_F(Setattr_7_8, chmod)
833
{
834
const char FULLPATH[] = "mountpoint/some_file.txt";
835
const char RELPATH[] = "some_file.txt";
836
const uint64_t ino = 42;
837
const mode_t oldmode = 0755;
838
const mode_t newmode = 0644;
839
840
EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
841
.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
842
SET_OUT_HEADER_LEN(out, entry_7_8);
843
out.body.entry.attr.mode = S_IFREG | oldmode;
844
out.body.entry.nodeid = ino;
845
})));
846
847
EXPECT_CALL(*m_mock, process(
848
ResultOf([](auto in) {
849
uint32_t valid = FATTR_MODE;
850
return (in.header.opcode == FUSE_SETATTR &&
851
in.header.nodeid == ino &&
852
in.body.setattr.valid == valid &&
853
in.body.setattr.mode == newmode);
854
}, Eq(true)),
855
_)
856
).WillOnce(Invoke(ReturnImmediate([](auto in __unused, auto& out) {
857
SET_OUT_HEADER_LEN(out, attr_7_8);
858
out.body.attr.attr.ino = ino; // Must match nodeid
859
out.body.attr.attr.mode = S_IFREG | newmode;
860
})));
861
EXPECT_EQ(0, chmod(FULLPATH, newmode)) << strerror(errno);
862
}
863
864