diff options
Diffstat (limited to 'test/fuse/linux/fuse_base.cc')
-rw-r--r-- | test/fuse/linux/fuse_base.cc | 447 |
1 files changed, 343 insertions, 104 deletions
diff --git a/test/fuse/linux/fuse_base.cc b/test/fuse/linux/fuse_base.cc index 9c3124472..5b45804e1 100644 --- a/test/fuse/linux/fuse_base.cc +++ b/test/fuse/linux/fuse_base.cc @@ -16,17 +16,17 @@ #include <fcntl.h> #include <linux/fuse.h> -#include <string.h> +#include <poll.h> #include <sys/mount.h> +#include <sys/socket.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/uio.h> #include <unistd.h> -#include <iostream> - #include "gtest/gtest.h" #include "absl/strings/str_format.h" +#include "test/util/fuse_util.h" #include "test/util/posix_error.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" @@ -39,49 +39,123 @@ void FuseTest::SetUp() { SetUpFuseServer(); } -void FuseTest::TearDown() { UnmountFuse(); } - -// Since CompareRequest is running in background thread, gTest assertions and -// expectations won't directly reflect the test result. However, the FUSE -// background server still connects to the same standard I/O as testing main -// thread. So EXPECT_XX can still be used to show different results. To -// ensure failed testing result is observable, return false and the result -// will be sent to test main thread via pipe. -bool FuseTest::CompareRequest(void* expected_mem, size_t expected_len, - void* real_mem, size_t real_len) { - if (expected_len != real_len) return false; - return memcmp(expected_mem, real_mem, expected_len) == 0; +void FuseTest::TearDown() { + EXPECT_EQ(GetServerNumUnconsumedRequests(), 0); + EXPECT_EQ(GetServerNumUnsentResponses(), 0); + UnmountFuse(); } -// SetExpected is called by the testing main thread to set expected request- -// response pair of a single FUSE operation. -void FuseTest::SetExpected(struct iovec* iov_in, int iov_in_cnt, - struct iovec* iov_out, int iov_out_cnt) { - EXPECT_THAT(RetryEINTR(writev)(set_expected_[1], iov_in, iov_in_cnt), - SyscallSucceedsWithValue(::testing::Gt(0))); - WaitCompleted(); +// Sends 3 parts of data to the FUSE server: +// 1. The `kSetResponse` command +// 2. The expected opcode +// 3. The fake FUSE response +// Then waits for the FUSE server to notify its completion. +void FuseTest::SetServerResponse(uint32_t opcode, + std::vector<struct iovec>& iovecs) { + uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSetResponse); + EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)), + SyscallSucceedsWithValue(sizeof(cmd))); + + EXPECT_THAT(RetryEINTR(write)(sock_[0], &opcode, sizeof(opcode)), + SyscallSucceedsWithValue(sizeof(opcode))); - EXPECT_THAT(RetryEINTR(writev)(set_expected_[1], iov_out, iov_out_cnt), - SyscallSucceedsWithValue(::testing::Gt(0))); - WaitCompleted(); + EXPECT_THAT(RetryEINTR(writev)(sock_[0], iovecs.data(), iovecs.size()), + SyscallSucceeds()); + + WaitServerComplete(); } -// WaitCompleted waits for the FUSE server to finish its job and check if it +// Waits for the FUSE server to finish its blocking job and check if it // completes without errors. -void FuseTest::WaitCompleted() { - char success; - EXPECT_THAT(RetryEINTR(read)(done_[0], &success, sizeof(success)), - SyscallSucceedsWithValue(1)); +void FuseTest::WaitServerComplete() { + uint32_t success; + EXPECT_THAT(RetryEINTR(read)(sock_[0], &success, sizeof(success)), + SyscallSucceedsWithValue(sizeof(success))); + ASSERT_EQ(success, 1); +} + +// Sends the `kGetRequest` command to the FUSE server, then reads the next +// request into iovec struct. The order of calling this function should be +// the same as the one of SetServerResponse(). +void FuseTest::GetServerActualRequest(std::vector<struct iovec>& iovecs) { + uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kGetRequest); + EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)), + SyscallSucceedsWithValue(sizeof(cmd))); + + EXPECT_THAT(RetryEINTR(readv)(sock_[0], iovecs.data(), iovecs.size()), + SyscallSucceeds()); + + WaitServerComplete(); +} + +// Sends a FuseTestCmd command to the FUSE server, reads from the socket, and +// returns the corresponding data. +uint32_t FuseTest::GetServerData(uint32_t cmd) { + uint32_t data; + EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)), + SyscallSucceedsWithValue(sizeof(cmd))); + + EXPECT_THAT(RetryEINTR(read)(sock_[0], &data, sizeof(data)), + SyscallSucceedsWithValue(sizeof(data))); + + WaitServerComplete(); + return data; +} + +uint32_t FuseTest::GetServerNumUnconsumedRequests() { + return GetServerData( + static_cast<uint32_t>(FuseTestCmd::kGetNumUnconsumedRequests)); +} + +uint32_t FuseTest::GetServerNumUnsentResponses() { + return GetServerData( + static_cast<uint32_t>(FuseTestCmd::kGetNumUnsentResponses)); +} + +uint32_t FuseTest::GetServerTotalReceivedBytes() { + return GetServerData( + static_cast<uint32_t>(FuseTestCmd::kGetTotalReceivedBytes)); +} + +// Sends the `kSkipRequest` command to the FUSE server, which would skip +// current stored request data. +void FuseTest::SkipServerActualRequest() { + uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSkipRequest); + EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)), + SyscallSucceedsWithValue(sizeof(cmd))); + + WaitServerComplete(); } -void FuseTest::MountFuse() { +// Sends the `kSetInodeLookup` command, expected mode, and the path of the +// inode to create under the mount point. +void FuseTest::SetServerInodeLookup(const std::string& path, mode_t mode, + uint64_t size) { + uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSetInodeLookup); + EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)), + SyscallSucceedsWithValue(sizeof(cmd))); + + EXPECT_THAT(RetryEINTR(write)(sock_[0], &mode, sizeof(mode)), + SyscallSucceedsWithValue(sizeof(mode))); + + EXPECT_THAT(RetryEINTR(write)(sock_[0], &size, sizeof(size)), + SyscallSucceedsWithValue(sizeof(size))); + + // Pad 1 byte for null-terminate c-string. + EXPECT_THAT(RetryEINTR(write)(sock_[0], path.c_str(), path.size() + 1), + SyscallSucceedsWithValue(path.size() + 1)); + + WaitServerComplete(); +} + +void FuseTest::MountFuse(const char* mountOpts) { EXPECT_THAT(dev_fd_ = open("/dev/fuse", O_RDWR), SyscallSucceeds()); - std::string mount_opts = absl::StrFormat("fd=%d,%s", dev_fd_, kMountOpts); + std::string mount_opts = absl::StrFormat("fd=%d,%s", dev_fd_, mountOpts); mount_point_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); EXPECT_THAT(mount("fuse", mount_point_.path().c_str(), "fuse", MS_NODEV | MS_NOSUID, mount_opts.c_str()), - SyscallSucceedsWithValue(0)); + SyscallSucceeds()); } void FuseTest::UnmountFuse() { @@ -89,13 +163,13 @@ void FuseTest::UnmountFuse() { // TODO(gvisor.dev/issue/3330): ensure the process is terminated successfully. } -// ConsumeFuseInit consumes the first FUSE request and returns the -// corresponding PosixError. -PosixError FuseTest::ConsumeFuseInit() { +// Consumes the first FUSE request and returns the corresponding PosixError. +PosixError FuseTest::ServerConsumeFuseInit( + const struct fuse_init_out* out_payload) { + std::vector<char> buf(FUSE_MIN_READ_BUFFER); RETURN_ERROR_IF_SYSCALL_FAIL( - RetryEINTR(read)(dev_fd_, buf_.data(), buf_.size())); + RetryEINTR(read)(dev_fd_, buf.data(), buf.size())); - struct iovec iov_out[2]; struct fuse_out_header out_header = { .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_init_out), .error = 0, @@ -103,72 +177,74 @@ PosixError FuseTest::ConsumeFuseInit() { }; // Returns a fake fuse_init_out with 7.0 version to avoid ECONNREFUSED // error in the initialization of FUSE connection. - struct fuse_init_out out_payload = { - .major = 7, - }; - iov_out[0].iov_len = sizeof(out_header); - iov_out[0].iov_base = &out_header; - iov_out[1].iov_len = sizeof(out_payload); - iov_out[1].iov_base = &out_payload; + auto iov_out = FuseGenerateIovecs( + out_header, *const_cast<struct fuse_init_out*>(out_payload)); - RETURN_ERROR_IF_SYSCALL_FAIL(RetryEINTR(writev)(dev_fd_, iov_out, 2)); + RETURN_ERROR_IF_SYSCALL_FAIL( + RetryEINTR(writev)(dev_fd_, iov_out.data(), iov_out.size())); return NoError(); } -// ReceiveExpected reads 1 pair of expected fuse request-response `iovec`s -// from pipe and save them into member variables of this testing instance. -void FuseTest::ReceiveExpected() { - // Set expected fuse_in request. - EXPECT_THAT(len_in_ = RetryEINTR(read)(set_expected_[0], mem_in_.data(), - mem_in_.size()), - SyscallSucceedsWithValue(::testing::Gt(0))); - MarkDone(len_in_ > 0); +// Reads 1 expected opcode and a fake response from socket and save them into +// the serial buffer of this testing instance. +void FuseTest::ServerReceiveResponse() { + ssize_t len; + uint32_t opcode; + std::vector<char> buf(FUSE_MIN_READ_BUFFER); + EXPECT_THAT(RetryEINTR(read)(sock_[1], &opcode, sizeof(opcode)), + SyscallSucceedsWithValue(sizeof(opcode))); - // Set expected fuse_out response. - EXPECT_THAT(len_out_ = RetryEINTR(read)(set_expected_[0], mem_out_.data(), - mem_out_.size()), - SyscallSucceedsWithValue(::testing::Gt(0))); - MarkDone(len_out_ > 0); + EXPECT_THAT(len = RetryEINTR(read)(sock_[1], buf.data(), buf.size()), + SyscallSucceeds()); + + responses_.AddMemBlock(opcode, buf.data(), len); } -// MarkDone writes 1 byte of success indicator through pipe. -void FuseTest::MarkDone(bool success) { - char data = success ? 1 : 0; - EXPECT_THAT(RetryEINTR(write)(done_[1], &data, sizeof(data)), - SyscallSucceedsWithValue(1)); +// Writes 1 byte of success indicator through socket. +void FuseTest::ServerCompleteWith(bool success) { + uint32_t data = success ? 1 : 0; + ServerSendData(data); } -// FuseLoop is the implementation of the fake FUSE server. Read from /dev/fuse, -// compare the request by CompareRequest (use derived function if specified), -// and write the expected response to /dev/fuse. -void FuseTest::FuseLoop() { - bool success = true; - ssize_t len = 0; +// ServerFuseLoop is the implementation of the fake FUSE server. Monitors 2 +// file descriptors: /dev/fuse and sock_[1]. Events from /dev/fuse are FUSE +// requests and events from sock_[1] are FUSE testing commands, leading by +// a FuseTestCmd data to indicate the command. +void FuseTest::ServerFuseLoop() { + const int nfds = 2; + struct pollfd fds[nfds] = { + { + .fd = dev_fd_, + .events = POLL_IN | POLLHUP | POLLERR | POLLNVAL, + }, + { + .fd = sock_[1], + .events = POLL_IN | POLLHUP | POLLERR | POLLNVAL, + }, + }; + while (true) { - ReceiveExpected(); + ASSERT_THAT(poll(fds, nfds, -1), SyscallSucceeds()); - EXPECT_THAT(len = RetryEINTR(read)(dev_fd_, buf_.data(), buf_.size()), - SyscallSucceedsWithValue(len_in_)); - if (len != len_in_) success = false; + for (int fd_idx = 0; fd_idx < nfds; ++fd_idx) { + if (fds[fd_idx].revents == 0) continue; - if (!CompareRequest(buf_.data(), len_in_, mem_in_.data(), len_in_)) { - std::cerr << "the FUSE request is not expected" << std::endl; - success = false; + ASSERT_EQ(fds[fd_idx].revents, POLL_IN); + if (fds[fd_idx].fd == sock_[1]) { + ServerHandleCommand(); + } else if (fds[fd_idx].fd == dev_fd_) { + ServerProcessFuseRequest(); + } } - - EXPECT_THAT(len = RetryEINTR(write)(dev_fd_, mem_out_.data(), len_out_), - SyscallSucceedsWithValue(len_out_)); - if (len != len_out_) success = false; - MarkDone(success); } } -// SetUpFuseServer creates 2 pipes. First is for testing client to send the -// expected request-response pair, and the other acts as a checkpoint for the -// FUSE server to notify the client that it can proceed. -void FuseTest::SetUpFuseServer() { - ASSERT_THAT(pipe(set_expected_), SyscallSucceedsWithValue(0)); - ASSERT_THAT(pipe(done_), SyscallSucceedsWithValue(0)); +// SetUpFuseServer creates 1 socketpair and fork the process. The parent thread +// becomes testing thread and the child thread becomes the FUSE server running +// in background. These 2 threads are connected via socketpair. sock_[0] is +// opened in testing thread and sock_[1] is opened in the FUSE server. +void FuseTest::SetUpFuseServer(const struct fuse_init_out* payload) { + ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sock_), SyscallSucceeds()); switch (fork()) { case -1: @@ -177,31 +253,194 @@ void FuseTest::SetUpFuseServer() { case 0: break; default: - ASSERT_THAT(close(set_expected_[0]), SyscallSucceedsWithValue(0)); - ASSERT_THAT(close(done_[1]), SyscallSucceedsWithValue(0)); - WaitCompleted(); + ASSERT_THAT(close(sock_[1]), SyscallSucceeds()); + WaitServerComplete(); return; } - ASSERT_THAT(close(set_expected_[1]), SyscallSucceedsWithValue(0)); - ASSERT_THAT(close(done_[0]), SyscallSucceedsWithValue(0)); - - MarkDone(ConsumeFuseInit().ok()); - - FuseLoop(); + // Begin child thread, i.e. the FUSE server. + ASSERT_THAT(close(sock_[0]), SyscallSucceeds()); + ServerCompleteWith(ServerConsumeFuseInit(payload).ok()); + ServerFuseLoop(); _exit(0); } -// GetPayloadSize is a helper function to get the number of bytes of a -// specific FUSE operation struct. -size_t FuseTest::GetPayloadSize(uint32_t opcode, bool in) { - switch (opcode) { - case FUSE_INIT: - return in ? sizeof(struct fuse_init_in) : sizeof(struct fuse_init_out); +void FuseTest::ServerSendData(uint32_t data) { + EXPECT_THAT(RetryEINTR(write)(sock_[1], &data, sizeof(data)), + SyscallSucceedsWithValue(sizeof(data))); +} + +// Reads FuseTestCmd sent from testing thread and routes to correct handler. +// Since each command should be a blocking operation, a `ServerCompleteWith()` +// is required after the switch keyword. +void FuseTest::ServerHandleCommand() { + uint32_t cmd; + EXPECT_THAT(RetryEINTR(read)(sock_[1], &cmd, sizeof(cmd)), + SyscallSucceedsWithValue(sizeof(cmd))); + + switch (static_cast<FuseTestCmd>(cmd)) { + case FuseTestCmd::kSetResponse: + ServerReceiveResponse(); + break; + case FuseTestCmd::kSetInodeLookup: + ServerReceiveInodeLookup(); + break; + case FuseTestCmd::kGetRequest: + ServerSendReceivedRequest(); + break; + case FuseTestCmd::kGetTotalReceivedBytes: + ServerSendData(static_cast<uint32_t>(requests_.UsedBytes())); + break; + case FuseTestCmd::kGetNumUnconsumedRequests: + ServerSendData(static_cast<uint32_t>(requests_.RemainingBlocks())); + break; + case FuseTestCmd::kGetNumUnsentResponses: + ServerSendData(static_cast<uint32_t>(responses_.RemainingBlocks())); + break; + case FuseTestCmd::kSkipRequest: + ServerSkipReceivedRequest(); + break; default: + FAIL() << "Unknown FuseTestCmd " << cmd; break; } - return 0; + + ServerCompleteWith(!HasFailure()); +} + +// Reads the expected file mode and the path of one file. Crafts a basic +// `fuse_entry_out` memory block and inserts into a map for future use. +// The FUSE server will always return this response if a FUSE_LOOKUP +// request with this specific path comes in. +void FuseTest::ServerReceiveInodeLookup() { + mode_t mode; + uint64_t size; + std::vector<char> buf(FUSE_MIN_READ_BUFFER); + + EXPECT_THAT(RetryEINTR(read)(sock_[1], &mode, sizeof(mode)), + SyscallSucceedsWithValue(sizeof(mode))); + + EXPECT_THAT(RetryEINTR(read)(sock_[1], &size, sizeof(size)), + SyscallSucceedsWithValue(sizeof(size))); + + EXPECT_THAT(RetryEINTR(read)(sock_[1], buf.data(), buf.size()), + SyscallSucceeds()); + + std::string path(buf.data()); + + uint32_t out_len = + sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out); + struct fuse_out_header out_header = { + .len = out_len, + .error = 0, + }; + struct fuse_entry_out out_payload = DefaultEntryOut(mode, nodeid_); + // Since this is only used in test, nodeid_ is simply increased by 1 to + // comply with the unqiueness of different path. + ++nodeid_; + + // Set the size. + out_payload.attr.size = size; + + memcpy(buf.data(), &out_header, sizeof(out_header)); + memcpy(buf.data() + sizeof(out_header), &out_payload, sizeof(out_payload)); + lookups_.AddMemBlock(FUSE_LOOKUP, buf.data(), out_len); + lookup_map_[path] = lookups_.Next(); +} + +// Sends the received request pointed by current cursor and advances cursor. +void FuseTest::ServerSendReceivedRequest() { + if (requests_.End()) { + FAIL() << "No more received request."; + return; + } + auto mem_block = requests_.Next(); + EXPECT_THAT( + RetryEINTR(write)(sock_[1], requests_.DataAtOffset(mem_block.offset), + mem_block.len), + SyscallSucceedsWithValue(mem_block.len)); +} + +// Skip the request pointed by current cursor. +void FuseTest::ServerSkipReceivedRequest() { + if (requests_.End()) { + FAIL() << "No more received request."; + return; + } + requests_.Next(); +} + +// Handles FUSE request. Reads request from /dev/fuse, checks if it has the +// same opcode as expected, and responds with the saved fake FUSE response. +// The FUSE request is copied to the serial buffer and can be retrieved one- +// by-one by calling GetServerActualRequest from testing thread. +void FuseTest::ServerProcessFuseRequest() { + ssize_t len; + std::vector<char> buf(FUSE_MIN_READ_BUFFER); + + // Read FUSE request. + EXPECT_THAT(len = RetryEINTR(read)(dev_fd_, buf.data(), buf.size()), + SyscallSucceeds()); + fuse_in_header* in_header = reinterpret_cast<fuse_in_header*>(buf.data()); + + // Check if this is a preset FUSE_LOOKUP path. + if (in_header->opcode == FUSE_LOOKUP) { + std::string path(buf.data() + sizeof(struct fuse_in_header)); + auto it = lookup_map_.find(path); + if (it != lookup_map_.end()) { + // Matches a preset path. Reply with fake data and skip saving the + // request. + ServerRespondFuseSuccess(lookups_, it->second, in_header->unique); + return; + } + } + + requests_.AddMemBlock(in_header->opcode, buf.data(), len); + + if (in_header->opcode == FUSE_RELEASE || in_header->opcode == FUSE_RELEASEDIR) + return; + // Check if there is a corresponding response. + if (responses_.End()) { + GTEST_NONFATAL_FAILURE_("No more FUSE response is expected"); + ServerRespondFuseError(in_header->unique); + return; + } + auto mem_block = responses_.Next(); + if (in_header->opcode != mem_block.opcode) { + std::string message = absl::StrFormat("Expect opcode %d but got %d", + mem_block.opcode, in_header->opcode); + GTEST_NONFATAL_FAILURE_(message.c_str()); + // We won't get correct response if opcode is not expected. Send error + // response here to avoid wrong parsing by VFS. + ServerRespondFuseError(in_header->unique); + return; + } + + // Write FUSE response. + ServerRespondFuseSuccess(responses_, mem_block, in_header->unique); +} + +void FuseTest::ServerRespondFuseSuccess(FuseMemBuffer& mem_buf, + const FuseMemBlock& block, + uint64_t unique) { + fuse_out_header* out_header = + reinterpret_cast<fuse_out_header*>(mem_buf.DataAtOffset(block.offset)); + + // Patch `unique` in fuse_out_header to avoid EINVAL caused by responding + // with an unknown `unique`. + out_header->unique = unique; + EXPECT_THAT(RetryEINTR(write)(dev_fd_, out_header, block.len), + SyscallSucceedsWithValue(block.len)); +} + +void FuseTest::ServerRespondFuseError(uint64_t unique) { + fuse_out_header out_header = { + .len = sizeof(struct fuse_out_header), + .error = ENOSYS, + .unique = unique, + }; + EXPECT_THAT(RetryEINTR(write)(dev_fd_, &out_header, sizeof(out_header)), + SyscallSucceedsWithValue(sizeof(out_header))); } } // namespace testing |