// Copyright 2023 The gVisor Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" #include "test/util/linux_capability_util.h" #include "test/util/mount_util.h" #include "test/util/posix_error.h" #include "test/util/save_util.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" #include "test/util/thread_util.h" using ::testing::Ge; namespace gvisor { namespace testing { namespace { void FuseInit(int fd) { alignas(fuse_in_header) char req_buf[FUSE_MIN_READ_BUFFER]; ASSERT_THAT(read(fd, req_buf, sizeof(req_buf)), SyscallSucceedsWithValue(Ge(sizeof(fuse_in_header)))); fuse_in_header* in_hdr = reinterpret_cast(req_buf); ASSERT_EQ(in_hdr->opcode, FUSE_INIT); fuse_out_header out_hdr; out_hdr.error = 0; out_hdr.unique = in_hdr->unique; fuse_init_out out_payload = {}; out_payload.major = FUSE_KERNEL_VERSION; out_payload.minor = FUSE_KERNEL_MINOR_VERSION; out_payload.max_readahead = 0; out_payload.flags = 0; out_payload.congestion_threshold = 0; struct iovec iov[] = { {.iov_base = &out_hdr, .iov_len = sizeof(out_hdr)}, {.iov_base = &out_payload, .iov_len = sizeof(out_payload)}, }; out_hdr.len = sizeof(out_hdr) + sizeof(out_payload); ASSERT_THAT(writev(fd, iov, 2), SyscallSucceedsWithValue(sizeof(out_hdr) + sizeof(out_payload))); } TEST(FuseTest, RejectBadInit) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); const FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/fuse", O_RDWR, 0)); auto mount_point = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); auto mount_opts = absl::StrFormat("fd=%d,user_id=0,group_id=0,rootmode=40000", fd.get()); auto mount = ASSERT_NO_ERRNO_AND_VALUE( Mount("fuse", mount_point.path(), "fuse", MS_NODEV | MS_NOSUID, mount_opts, 0 /* umountflags */)); // Read the init request so that we have the correct unique ID. alignas(fuse_in_header) char req_buf[FUSE_MIN_READ_BUFFER]; ASSERT_THAT(read(fd.get(), req_buf, sizeof(req_buf)), SyscallSucceedsWithValue(Ge(sizeof(fuse_in_header)))); fuse_out_header resp; resp.len = sizeof(resp) - 1; resp.error = 0; resp.unique = reinterpret_cast(req_buf)->unique; ASSERT_THAT(write(fd.get(), reinterpret_cast(&resp), sizeof(resp)), SyscallFailsWithErrno(EINVAL)); } TEST(FuseTest, CloneDevice) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); SKIP_IF(IsRunningWithSaveRestore()); const FileDescriptor fd1 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/fuse", O_RDWR)); auto mount_point = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); auto mount_opts = absl::StrFormat("fd=%d,user_id=0,group_id=0,rootmode=40000", fd1.get()); auto mount = ASSERT_NO_ERRNO_AND_VALUE( Mount("fuse", mount_point.path(), "fuse", MS_NODEV | MS_NOSUID, mount_opts, 0 /* umountflags */)); FuseInit(fd1.get()); const FileDescriptor fd2 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/fuse", O_RDWR)); int fd1_num = fd1.get(); ASSERT_THAT(ioctl(fd2.get(), FUSE_DEV_IOC_CLONE, &fd1_num), SyscallSucceeds()); ScopedThread fuse_server = ScopedThread([&] { // Send stat reply from both FUSE servers. for (int fd : {fd1.get(), fd2.get()}) { // Read the stat request. alignas(fuse_in_header) char req_buf[4096 * 4]; ASSERT_THAT(read(fd, req_buf, sizeof(req_buf)), SyscallSucceedsWithValue(Ge(sizeof(fuse_in_header)))); fuse_in_header* in_hdr = reinterpret_cast(req_buf); ASSERT_EQ(in_hdr->opcode, FUSE_GETATTR); // Send stat reply. fuse_out_header out_hdr; out_hdr.error = 0; out_hdr.unique = in_hdr->unique; fuse_attr_out out_payload = {}; out_payload.attr.mode = S_IFDIR | 0755; out_payload.attr.nlink = 1; out_payload.attr.uid = 0; out_payload.attr.gid = 0; out_payload.attr.size = fd; out_payload.attr.atime = 0; out_payload.attr.mtime = 0; out_payload.attr.ctime = 0; struct iovec iov[] = { {.iov_base = &out_hdr, .iov_len = sizeof(out_hdr)}, {.iov_base = &out_payload, .iov_len = sizeof(out_payload)}, }; out_hdr.len = sizeof(out_hdr) + sizeof(out_payload); ASSERT_THAT( writev(fd, iov, 2), SyscallSucceedsWithValue(sizeof(out_hdr) + sizeof(out_payload))); } }); // Check if filesystem is responsive by stat'ing root. Both FUSE servers // should be able to respond. struct stat st; EXPECT_THAT(stat(mount_point.path().c_str(), &st), SyscallSucceeds()); EXPECT_EQ(st.st_size, fd1.get()); EXPECT_THAT(stat(mount_point.path().c_str(), &st), SyscallSucceeds()); EXPECT_EQ(st.st_size, fd2.get()); fuse_server.Join(); } TEST(FuseTest, CloneToConnectedDeviceFails) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); const FileDescriptor fd1 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/fuse", O_RDWR)); auto mount_point = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); auto mount_opts = absl::StrFormat("fd=%d,user_id=0,group_id=0,rootmode=40000", fd1.get()); auto mount = ASSERT_NO_ERRNO_AND_VALUE( Mount("fuse", mount_point.path(), "fuse", MS_NODEV | MS_NOSUID, mount_opts, 0 /* umountflags */)); FuseInit(fd1.get()); int fd1_num = fd1.get(); EXPECT_THAT(ioctl(fd1.get(), FUSE_DEV_IOC_CLONE, &fd1_num), SyscallFailsWithErrno(EINVAL)); } TEST(FuseTest, CloneFromUnconnectedDeviceFails) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); const FileDescriptor fd1 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/fuse", O_RDWR)); const FileDescriptor fd2 = ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/fuse", O_RDWR)); int fd1_num = fd1.get(); EXPECT_THAT(ioctl(fd2.get(), FUSE_DEV_IOC_CLONE, &fd1_num), SyscallFailsWithErrno(EINVAL)); } TEST(FuseTest, LookupUpdatesInode) { SKIP_IF(absl::NullSafeStringView(getenv("GVISOR_FUSE_TEST")) != "TRUE"); const std::string kFileData = "May thy knife chip and shatter.\n"; TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( GetAbsoluteTestTmpdir(), kFileData, TempPath::kDefaultFileMode)); FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_RDONLY)); std::vector buf(kFileData.size()); ASSERT_THAT(ReadFd(fd.get(), buf.data(), kFileData.size()), SyscallSucceedsWithValue(kFileData.size())); ASSERT_THAT(unlink(JoinPath("/fuse", Basename(path.path())).c_str()), SyscallSucceeds()); EXPECT_THAT(access(path.path().c_str(), O_RDONLY), SyscallFailsWithErrno(ENOENT)); } } // namespace } // namespace testing } // namespace gvisor