root/tests/sys/fs/fusefs/nfs.cc
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2019 The FreeBSD Foundation
 *
 * This software was developed by BFF Storage Systems, LLC under sponsorship
 * from the FreeBSD Foundation.
 *
 * 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.
 */

/* This file tests functionality needed by NFS servers */
extern "C" {
#include <sys/param.h>
#include <sys/mount.h>

#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
}

#include "mockfs.hh"
#include "utils.hh"

using namespace std;
using namespace testing;


class Nfs: public FuseTest {
public:
virtual void SetUp() {
        if (geteuid() != 0)
                GTEST_SKIP() << "This test requires a privileged user";
        FuseTest::SetUp();
}
};

class Exportable: public Nfs {
public:
virtual void SetUp() {
        m_init_flags = FUSE_EXPORT_SUPPORT;
        Nfs::SetUp();
}
};

class Fhstat: public Exportable {};
class FhstatNotExportable: public Nfs {};
class Getfh: public Exportable {};
class Readdir: public Exportable {};

/* If the server returns a different generation number, then file is stale */
TEST_F(Fhstat, estale)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t ino = 42;
        const mode_t mode = S_IFDIR | 0755;
        Sequence seq;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .InSequence(seq)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        EXPECT_LOOKUP(ino, ".")
        .InSequence(seq)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 2;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        ASSERT_EQ(-1, fhstat(&fhp, &sb));
        EXPECT_EQ(ESTALE, errno);
}

/* If we must lookup an entry from the server, send a LOOKUP request for "." */
TEST_F(Fhstat, lookup_dot)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t ino = 42;
        const mode_t mode = S_IFDIR | 0755;
        const uid_t uid = 12345;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        EXPECT_LOOKUP(ino, ".")
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
        EXPECT_EQ(uid, sb.st_uid);
        EXPECT_EQ(mode, sb.st_mode);
}

/* Gracefully handle failures to lookup ".". */
TEST_F(Fhstat, lookup_dot_error)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t ino = 42;
        const mode_t mode = S_IFDIR | 0755;
        const uid_t uid = 12345;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        EXPECT_LOOKUP(ino, ".")
        .WillOnce(Invoke(ReturnErrno(EDOOFUS)));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        ASSERT_EQ(-1, fhstat(&fhp, &sb));
        EXPECT_EQ(EDOOFUS, errno);
}

/* Use a file handle whose entry is still cached */
TEST_F(Fhstat, cached)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t ino = 42;
        const mode_t mode = S_IFDIR | 0755;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
        EXPECT_EQ(ino, sb.st_ino);
}

/* File handle entries should expire from the cache, too */
TEST_F(Fhstat, cache_expired)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t ino = 42;
        const mode_t mode = S_IFDIR | 0755;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid_nsec = NAP_NS / 2;
        })));

        EXPECT_LOOKUP(ino, ".")
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
        EXPECT_EQ(ino, sb.st_ino);

        nap();

        /* Cache should be expired; fuse should issue a FUSE_LOOKUP */
        ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
        EXPECT_EQ(ino, sb.st_ino);
}

/*
 * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't
 * lookup, it's a bug.  But we should handle it gracefully.
 */
TEST_F(Fhstat, inconsistent_nodeid)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t ino_in = 42;
        const uint64_t ino_out = 43;
        const mode_t mode = S_IFDIR | 0755;
        const uid_t uid = 12345;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = ino_in;
                out.body.entry.attr.ino = ino_in;
                out.body.entry.attr.mode = mode;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        EXPECT_LOOKUP(ino_in, ".")
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = ino_out;
                out.body.entry.attr.ino = ino_out;
                out.body.entry.attr.mode = mode;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno);
        EXPECT_EQ(EIO, errno);
}

/*
 * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match
 * the inode number, and the file system is exported, it's a bug.  But we
 * should handle it gracefully.
 */
TEST_F(Fhstat, inconsistent_ino)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        struct stat sb;
        const uint64_t nodeid = 42;
        const uint64_t ino = 711;       // Could be anything that != nodeid
        const mode_t mode = S_IFDIR | 0755;
        const uid_t uid = 12345;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = nodeid;
                out.body.entry.attr.ino = nodeid;
                out.body.entry.attr.mode = mode;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        EXPECT_LOOKUP(nodeid, ".")
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.nodeid = nodeid;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr.mode = mode;
                out.body.entry.generation = 1;
                out.body.entry.attr.uid = uid;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        /*
         * The fhstat operation will actually succeed.  But future operations
         * will likely fail.
         */
        ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
        EXPECT_EQ(ino, sb.st_ino);
}

/* 
 * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
 * lookups
 */
TEST_F(FhstatNotExportable, lookup_dot)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        const uint64_t ino = 42;
        const mode_t mode = S_IFDIR | 0755;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        ASSERT_EQ(-1, getfh(FULLPATH, &fhp));
        ASSERT_EQ(EOPNOTSUPP, errno);
}

/* FreeBSD's fid struct doesn't have enough space for 64-bit generations */
TEST_F(Getfh, eoverflow)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        uint64_t ino = 42;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));

        ASSERT_NE(0, getfh(FULLPATH, &fhp));
        EXPECT_EQ(EOVERFLOW, errno);
}

/* Get an NFS file handle */
TEST_F(Getfh, ok)
{
        const char FULLPATH[] = "mountpoint/some_dir/.";
        const char RELDIRPATH[] = "some_dir";
        fhandle_t fhp;
        uint64_t ino = 42;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = S_IFDIR | 0755;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = UINT64_MAX;
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
}

/* 
 * Call readdir via a file handle.
 *
 * This is how a userspace nfs server like nfs-ganesha or unfs3 would call
 * readdir.  The in-kernel NFS server never does any equivalent of open.  I
 * haven't discovered a way to mimic nfsd's behavior short of actually running
 * nfsd.
 */
TEST_F(Readdir, getdirentries)
{
        const char FULLPATH[] = "mountpoint/some_dir";
        const char RELPATH[] = "some_dir";
        uint64_t ino = 42;
        mode_t mode = S_IFDIR | 0755;
        fhandle_t fhp;
        int fd;
        char buf[8192];
        ssize_t r;

        EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        EXPECT_LOOKUP(ino, ".")
        .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                SET_OUT_HEADER_LEN(out, entry);
                out.body.entry.attr.mode = mode;
                out.body.entry.nodeid = ino;
                out.body.entry.attr.ino = ino;
                out.body.entry.generation = 1;
                out.body.entry.attr_valid = UINT64_MAX;
                out.body.entry.entry_valid = 0;
        })));

        expect_opendir(ino);

        EXPECT_CALL(*m_mock, process(
                ResultOf([=](auto in) {
                        return (in.header.opcode == FUSE_READDIR &&
                                in.header.nodeid == ino &&
                                in.body.readdir.size == sizeof(buf));
                }, Eq(true)),
                _)
        ).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
                out.header.error = 0;
                out.header.len = sizeof(out.header);
        })));

        ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
        fd = fhopen(&fhp, O_DIRECTORY);
        ASSERT_LE(0, fd) << strerror(errno);
        r = getdirentries(fd, buf, sizeof(buf), 0);
        ASSERT_EQ(0, r) << strerror(errno);

        leak(fd);
}