Path: blob/main/tests/sys/fs/fusefs/last_local_modify.cc
39537 views
/*-1* SPDX-License-Identifier: BSD-2-Clause2*3* Copyright (c) 2021 Alan Somers4*5* Redistribution and use in source and binary forms, with or without6* modification, are permitted provided that the following conditions7* are met:8* 1. Redistributions of source code must retain the above copyright9* notice, this list of conditions and the following disclaimer.10* 2. Redistributions in binary form must reproduce the above copyright11* notice, this list of conditions and the following disclaimer in the12* documentation and/or other materials provided with the distribution.13*14* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND15* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE16* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE17* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE18* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL19* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS20* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)21* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT22* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY23* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF24* SUCH DAMAGE.25*/2627extern "C" {28#include <sys/param.h>29#include <sys/mount.h>30#include <sys/stat.h>3132#include <fcntl.h>33#include <pthread.h>34#include <semaphore.h>35}3637#include "mockfs.hh"38#include "utils.hh"3940using namespace testing;4142/*43* "Last Local Modify" bugs44*45* This file tests a class of race conditions caused by one thread fetching a46* file's size with FUSE_LOOKUP while another thread simultaneously modifies it47* with FUSE_SETATTR, FUSE_WRITE, FUSE_COPY_FILE_RANGE or similar. It's48* possible for the second thread to start later yet finish first. If that49* happens, the first thread must not override the size set by the second50* thread.51*52* FUSE_GETATTR is not vulnerable to the same race, because it is always called53* with the vnode lock held.54*55* A few other operations like FUSE_LINK can also trigger the same race but56* with the file's ctime instead of size. However, the consequences of an57* incorrect ctime are much less disastrous than an incorrect size, so fusefs58* does not attempt to prevent such races.59*/6061enum Mutator {62VOP_ALLOCATE,63VOP_COPY_FILE_RANGE,64VOP_SETATTR,65VOP_WRITE,66};6768/*69* Translate a poll method's string representation to the enum value.70* Using strings with ::testing::Values gives better output with71* --gtest_list_tests72*/73enum Mutator writer_from_str(const char* s) {74if (0 == strcmp("VOP_ALLOCATE", s))75return VOP_ALLOCATE;76else if (0 == strcmp("VOP_COPY_FILE_RANGE", s))77return VOP_COPY_FILE_RANGE;78else if (0 == strcmp("VOP_SETATTR", s))79return VOP_SETATTR;80else81return VOP_WRITE;82}8384uint32_t fuse_op_from_mutator(enum Mutator mutator) {85switch(mutator) {86case VOP_ALLOCATE:87return(FUSE_FALLOCATE);88case VOP_COPY_FILE_RANGE:89return(FUSE_COPY_FILE_RANGE);90case VOP_SETATTR:91return(FUSE_SETATTR);92case VOP_WRITE:93return(FUSE_WRITE);94}95}9697class LastLocalModify: public FuseTest, public WithParamInterface<const char*> {98public:99virtual void SetUp() {100m_init_flags = FUSE_EXPORT_SUPPORT;101102FuseTest::SetUp();103}104};105106static void* allocate_th(void* arg) {107int fd;108ssize_t r;109sem_t *sem = (sem_t*) arg;110111if (sem)112sem_wait(sem);113114fd = open("mountpoint/some_file.txt", O_RDWR);115if (fd < 0)116return (void*)(intptr_t)errno;117118r = posix_fallocate(fd, 0, 15);119LastLocalModify::leak(fd);120if (r >= 0)121return 0;122else123return (void*)(intptr_t)errno;124}125126static void* copy_file_range_th(void* arg) {127ssize_t r;128int fd;129sem_t *sem = (sem_t*) arg;130off_t off_in = 0;131off_t off_out = 10;132ssize_t len = 5;133134if (sem)135sem_wait(sem);136fd = open("mountpoint/some_file.txt", O_RDWR);137if (fd < 0)138return (void*)(intptr_t)errno;139140r = copy_file_range(fd, &off_in, fd, &off_out, len, 0);141if (r >= 0) {142LastLocalModify::leak(fd);143return 0;144} else145return (void*)(intptr_t)errno;146}147148static void* setattr_th(void* arg) {149int fd;150ssize_t r;151sem_t *sem = (sem_t*) arg;152153if (sem)154sem_wait(sem);155156fd = open("mountpoint/some_file.txt", O_RDWR);157if (fd < 0)158return (void*)(intptr_t)errno;159160r = ftruncate(fd, 15);161LastLocalModify::leak(fd);162if (r >= 0)163return 0;164else165return (void*)(intptr_t)errno;166}167168static void* write_th(void* arg) {169ssize_t r;170int fd;171sem_t *sem = (sem_t*) arg;172const char BUF[] = "abcdefghijklmn";173174if (sem)175sem_wait(sem);176/*177* Open the file in direct mode.178* The race condition affects both direct and non-direct writes, and179* they have separate code paths. However, in the non-direct case, the180* kernel updates last_local_modify _before_ sending FUSE_WRITE to the181* server. So the technique that this test program uses to invoke the182* race cannot work. Therefore, test with O_DIRECT only.183*/184fd = open("mountpoint/some_file.txt", O_RDWR | O_DIRECT);185if (fd < 0)186return (void*)(intptr_t)errno;187188r = write(fd, BUF, sizeof(BUF));189if (r >= 0) {190LastLocalModify::leak(fd);191return 0;192} else193return (void*)(intptr_t)errno;194}195196/*197* VOP_LOOKUP should discard attributes returned by the server if they were198* modified by another VOP while the VOP_LOOKUP was in progress.199*200* Sequence of operations:201* * Thread 1 calls a mutator like ftruncate, which acquires the vnode lock202* exclusively.203* * Thread 2 calls stat, which does VOP_LOOKUP, which sends FUSE_LOOKUP to the204* server. The server replies with the old file length. Thread 2 blocks205* waiting for the vnode lock.206* * Thread 1 sends the mutator operation like FUSE_SETATTR that changes the207* file's size and updates the attribute cache. Then it releases the vnode208* lock.209* * Thread 2 acquires the vnode lock. At this point it must not add the210* now-stale file size to the attribute cache.211*212* Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071213*/214TEST_P(LastLocalModify, lookup)215{216const char FULLPATH[] = "mountpoint/some_file.txt";217const char RELPATH[] = "some_file.txt";218Sequence seq;219uint64_t ino = 3;220uint64_t mutator_unique;221const uint64_t oldsize = 10;222const uint64_t newsize = 15;223pthread_t th0;224void *thr0_value;225struct stat sb;226static sem_t sem;227Mutator mutator;228uint32_t mutator_op;229size_t mutator_size;230231mutator = writer_from_str(GetParam());232mutator_op = fuse_op_from_mutator(mutator);233234ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);235236EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)237.InSequence(seq)238.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {239/* Called by the mutator, caches attributes but not entries */240SET_OUT_HEADER_LEN(out, entry);241out.body.entry.nodeid = ino;242out.body.entry.attr.size = oldsize;243out.body.entry.attr_valid_nsec = NAP_NS / 2;244out.body.entry.attr.ino = ino;245out.body.entry.attr.mode = S_IFREG | 0644;246})));247expect_open(ino, 0, 1);248EXPECT_CALL(*m_mock, process(249ResultOf([=](auto in) {250return (in.header.opcode == mutator_op &&251in.header.nodeid == ino);252}, Eq(true)),253_)254).InSequence(seq)255.WillOnce(Invoke([&](auto in, auto &out __unused) {256/*257* The mutator changes the file size, but in order to simulate258* a race, don't reply. Instead, just save the unique for259* later.260*/261mutator_unique = in.header.unique;262switch(mutator) {263case VOP_WRITE:264mutator_size = in.body.write.size;265break;266case VOP_COPY_FILE_RANGE:267mutator_size = in.body.copy_file_range.len;268break;269default:270break;271}272/* Allow the lookup thread to proceed */273sem_post(&sem);274}));275EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)276.InSequence(seq)277.WillOnce(Invoke([&](auto in __unused, auto& out) {278std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);279std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);280281/* First complete the lookup request, returning the old size */282out0->header.unique = in.header.unique;283SET_OUT_HEADER_LEN(*out0, entry);284out0->body.entry.attr.mode = S_IFREG | 0644;285out0->body.entry.nodeid = ino;286out0->body.entry.attr.ino = ino;287out0->body.entry.entry_valid = UINT64_MAX;288out0->body.entry.attr_valid = UINT64_MAX;289out0->body.entry.attr.size = oldsize;290out.push_back(std::move(out0));291292/* Then, respond to the mutator request */293out1->header.unique = mutator_unique;294switch(mutator) {295case VOP_ALLOCATE:296out1->header.error = 0;297out1->header.len = sizeof(out1->header);298break;299case VOP_COPY_FILE_RANGE:300SET_OUT_HEADER_LEN(*out1, write);301out1->body.write.size = mutator_size;302break;303case VOP_SETATTR:304SET_OUT_HEADER_LEN(*out1, attr);305out1->body.attr.attr.ino = ino;306out1->body.attr.attr.mode = S_IFREG | 0644;307out1->body.attr.attr.size = newsize; // Changed size308out1->body.attr.attr_valid = UINT64_MAX;309break;310case VOP_WRITE:311SET_OUT_HEADER_LEN(*out1, write);312out1->body.write.size = mutator_size;313break;314}315out.push_back(std::move(out1));316}));317318/* Start the mutator thread */319switch(mutator) {320case VOP_ALLOCATE:321ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,322NULL)) << strerror(errno);323break;324case VOP_COPY_FILE_RANGE:325ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,326NULL)) << strerror(errno);327break;328case VOP_SETATTR:329ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th, NULL))330<< strerror(errno);331break;332case VOP_WRITE:333ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, NULL))334<< strerror(errno);335break;336}337338339/* Wait for FUSE_SETATTR to be sent */340sem_wait(&sem);341342/* Lookup again, which will race with the mutator */343ASSERT_EQ(0, stat(FULLPATH, &sb)) << strerror(errno);344ASSERT_EQ((off_t)newsize, sb.st_size);345346/* ftruncate should've completed without error */347pthread_join(th0, &thr0_value);348EXPECT_EQ(0, (intptr_t)thr0_value);349}350351/*352* VFS_VGET should discard attributes returned by the server if they were353* modified by another VOP while the VFS_VGET was in progress.354*355* Sequence of operations:356* * Thread 1 calls fhstat, entering VFS_VGET, and issues FUSE_LOOKUP357* * Thread 2 calls a mutator like ftruncate, which acquires the vnode lock358* exclusively and issues a FUSE op like FUSE_SETATTR.359* * Thread 1's FUSE_LOOKUP returns with the old size, but the thread blocks360* waiting for the vnode lock.361* * Thread 2's FUSE op returns, and that thread sets the file's new size362* in the attribute cache. Finally it releases the vnode lock.363* * The vnode lock acquired, thread 1 must not overwrite the attr cache's size364* with the old value.365*366* Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=259071367*/368TEST_P(LastLocalModify, vfs_vget)369{370const char FULLPATH[] = "mountpoint/some_file.txt";371const char RELPATH[] = "some_file.txt";372Sequence seq;373uint64_t ino = 3;374uint64_t lookup_unique;375const uint64_t oldsize = 10;376const uint64_t newsize = 15;377pthread_t th0;378void *thr0_value;379struct stat sb;380static sem_t sem;381fhandle_t fhp;382Mutator mutator;383uint32_t mutator_op;384385if (geteuid() != 0)386GTEST_SKIP() << "This test requires a privileged user";387388mutator = writer_from_str(GetParam());389mutator_op = fuse_op_from_mutator(mutator);390391ASSERT_EQ(0, sem_init(&sem, 0, 0)) << strerror(errno);392393EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)394.Times(1)395.InSequence(seq)396.WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out)397{398/* Called by getfh, caches attributes but not entries */399SET_OUT_HEADER_LEN(out, entry);400out.body.entry.nodeid = ino;401out.body.entry.attr.size = oldsize;402out.body.entry.attr_valid_nsec = NAP_NS / 2;403out.body.entry.attr.ino = ino;404out.body.entry.attr.mode = S_IFREG | 0644;405})));406EXPECT_LOOKUP(ino, ".")407.InSequence(seq)408.WillOnce(Invoke([&](auto in, auto &out __unused) {409/* Called by fhstat. Block to simulate a race */410lookup_unique = in.header.unique;411sem_post(&sem);412}));413414EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)415.Times(1)416.InSequence(seq)417.WillRepeatedly(Invoke(ReturnImmediate([=](auto in __unused, auto& out)418{419/* Called by VOP_SETATTR, caches attributes but not entries */420SET_OUT_HEADER_LEN(out, entry);421out.body.entry.nodeid = ino;422out.body.entry.attr.size = oldsize;423out.body.entry.attr_valid_nsec = NAP_NS / 2;424out.body.entry.attr.ino = ino;425out.body.entry.attr.mode = S_IFREG | 0644;426})));427428/* Called by the mutator thread */429expect_open(ino, 0, 1);430431EXPECT_CALL(*m_mock, process(432ResultOf([=](auto in) {433return (in.header.opcode == mutator_op &&434in.header.nodeid == ino);435}, Eq(true)),436_)437).InSequence(seq)438.WillOnce(Invoke([&](auto in __unused, auto& out) {439std::unique_ptr<mockfs_buf_out> out0(new mockfs_buf_out);440std::unique_ptr<mockfs_buf_out> out1(new mockfs_buf_out);441442/* First complete the lookup request, returning the old size */443out0->header.unique = lookup_unique;444SET_OUT_HEADER_LEN(*out0, entry);445out0->body.entry.attr.mode = S_IFREG | 0644;446out0->body.entry.nodeid = ino;447out0->body.entry.attr.ino = ino;448out0->body.entry.entry_valid = UINT64_MAX;449out0->body.entry.attr_valid = UINT64_MAX;450out0->body.entry.attr.size = oldsize;451out.push_back(std::move(out0));452453/* Then, respond to the mutator request */454out1->header.unique = in.header.unique;455switch(mutator) {456case VOP_ALLOCATE:457out1->header.error = 0;458out1->header.len = sizeof(out1->header);459break;460case VOP_COPY_FILE_RANGE:461SET_OUT_HEADER_LEN(*out1, write);462out1->body.write.size = in.body.copy_file_range.len;463break;464case VOP_SETATTR:465SET_OUT_HEADER_LEN(*out1, attr);466out1->body.attr.attr.ino = ino;467out1->body.attr.attr.mode = S_IFREG | 0644;468out1->body.attr.attr.size = newsize; // Changed size469out1->body.attr.attr_valid = UINT64_MAX;470break;471case VOP_WRITE:472SET_OUT_HEADER_LEN(*out1, write);473out1->body.write.size = in.body.write.size;474break;475}476out.push_back(std::move(out1));477}));478479/* First get a file handle */480ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);481482/* Start the mutator thread */483switch(mutator) {484case VOP_ALLOCATE:485ASSERT_EQ(0, pthread_create(&th0, NULL, allocate_th,486(void*)&sem)) << strerror(errno);487break;488case VOP_COPY_FILE_RANGE:489ASSERT_EQ(0, pthread_create(&th0, NULL, copy_file_range_th,490(void*)&sem)) << strerror(errno);491break;492case VOP_SETATTR:493ASSERT_EQ(0, pthread_create(&th0, NULL, setattr_th,494(void*)&sem)) << strerror(errno);495break;496case VOP_WRITE:497ASSERT_EQ(0, pthread_create(&th0, NULL, write_th, (void*)&sem))498<< strerror(errno);499break;500}501502/* Lookup again, which will race with setattr */503ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);504505ASSERT_EQ((off_t)newsize, sb.st_size);506507/* mutator should've completed without error */508pthread_join(th0, &thr0_value);509EXPECT_EQ(0, (intptr_t)thr0_value);510}511512513INSTANTIATE_TEST_SUITE_P(LLM, LastLocalModify,514Values(515"VOP_ALLOCATE",516"VOP_COPY_FILE_RANGE",517"VOP_SETATTR",518"VOP_WRITE"519)520);521522523