aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/fusefs/lseek.cc
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/fusefs/lseek.cc')
-rw-r--r--tests/sys/fs/fusefs/lseek.cc518
1 files changed, 518 insertions, 0 deletions
diff --git a/tests/sys/fs/fusefs/lseek.cc b/tests/sys/fs/fusefs/lseek.cc
new file mode 100644
index 000000000000..12d41f7af1b2
--- /dev/null
+++ b/tests/sys/fs/fusefs/lseek.cc
@@ -0,0 +1,518 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Alan Somers
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+extern "C" {
+#include <sys/param.h>
+
+#include <fcntl.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+class Lseek: public FuseTest {};
+class LseekPathconf: public Lseek {};
+class LseekPathconf_7_23: public LseekPathconf {
+public:
+virtual void SetUp() {
+ m_kernel_minor_version = 23;
+ FuseTest::SetUp();
+}
+};
+class LseekSeekHole: public Lseek {};
+class LseekSeekData: public Lseek {};
+
+/*
+ * If a previous lseek operation has already returned enosys, then pathconf can
+ * return EINVAL immediately.
+ */
+TEST_F(LseekPathconf, already_enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+/*
+ * If a previous lseek operation has already returned successfully, then
+ * pathconf can return 1 immediately. 1 means "holes are reported, but size is
+ * not specified".
+ */
+TEST_F(LseekPathconf, already_seeked)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = i.body.lseek.offset;
+ })));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA));
+
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+
+ leak(fd);
+}
+
+/*
+ * Use pathconf on a file not already opened. The server returns EACCES when
+ * the kernel tries to open it. The kernel should return EACCES, and make no
+ * judgement about whether the server does or does not support FUSE_LSEEK.
+ */
+TEST_F(LseekPathconf, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.size = fsize;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnErrno(EACCES)));
+
+ EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EACCES, errno);
+ /* Check again, to ensure that the kernel didn't record the response */
+ EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EACCES, errno);
+}
+
+/*
+ * If the server returns some weird error when we try FUSE_LSEEK, send that to
+ * the caller but don't record the answer.
+ */
+TEST_F(LseekPathconf, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnErrno(EIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EIO, errno);
+ /* Check again, to ensure that the kernel didn't record the response */
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
+/*
+ * If no FUSE_LSEEK operation has been attempted since mount, try once as soon
+ * as a pathconf request comes in.
+ */
+TEST_F(LseekPathconf, enosys_now)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+/*
+ * Use pathconf, rather than fpathconf, on a file not already opened.
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135
+ */
+TEST_F(LseekPathconf, pathconf)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_out = 1 << 29;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ expect_release(ino, FuseTest::FH);
+
+ EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno);
+}
+
+/*
+ * If no FUSE_LSEEK operation has been attempted since mount, try one as soon
+ * as a pathconf request comes in. This is the typical pattern of bsdtar. It
+ * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
+ */
+TEST_F(LseekPathconf, seek_now)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_initial = 1 << 27;
+ off_t offset_out = 1 << 29;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ /* And check that the file pointer hasn't changed */
+ EXPECT_EQ(offset_initial, lseek(fd, 0, SEEK_CUR));
+
+ leak(fd);
+}
+
+/*
+ * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or
+ * zero-length file, then SEEK_DATA will return ENXIO. That should be
+ * interpreted as success.
+ */
+TEST_F(LseekPathconf, zerolength)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 0;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENXIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ /* Check again, to ensure that the kernel recorded the response */
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+
+ leak(fd);
+}
+
+/*
+ * For servers using older protocol versions, no FUSE_LSEEK should be attempted
+ */
+TEST_F(LseekPathconf_7_23, already_enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EINVAL, errno);
+
+ leak(fd);
+}
+
+TEST_F(LseekSeekData, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ off_t offset_out = 1 << 29;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ fd = open(FULLPATH, O_RDONLY);
+ EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_DATA));
+ EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
+
+ leak(fd);
+}
+
+/*
+ * If the server returns ENOSYS, fusefs should fall back to the default
+ * behavior, and never query the server again.
+ */
+TEST_F(LseekSeekData, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ /*
+ * Default behavior: ENXIO if offset is < 0 or >= fsize, offset
+ * otherwise.
+ */
+ EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
+ EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+ EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+
+ leak(fd);
+}
+
+TEST_F(LseekSeekHole, ok)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ off_t offset_out = 1 << 29;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_HOLE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
+ EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
+
+ leak(fd);
+}
+
+/*
+ * If the server returns ENOSYS, fusefs should fall back to the default
+ * behavior, and never query the server again.
+ */
+TEST_F(LseekSeekHole, enosys)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = 1 << 28;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_HOLE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENOSYS)));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ /*
+ * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
+ * otherwise.
+ */
+ EXPECT_EQ(fsize, lseek(fd, offset_in, SEEK_HOLE));
+ EXPECT_EQ(-1, lseek(fd, -1, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+ EXPECT_EQ(-1, lseek(fd, fsize, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+
+ leak(fd);
+}
+
+/* lseek should return ENXIO when offset points to EOF */
+TEST_F(LseekSeekHole, enxio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_in = fsize;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.fh == FH &&
+ (off_t)in.body.lseek.offset == offset_in &&
+ in.body.lseek.whence == SEEK_HOLE);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENXIO)));
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
+ EXPECT_EQ(ENXIO, errno);
+
+ leak(fd);
+}