blob: 64cf603facc69cf24246282a046f9f496fa590c9 [file]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fidl/fuchsia.io.admin/cpp/markers.h>
#include <fidl/fuchsia.io.admin/cpp/wire_test_base.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <fidl/fuchsia.io/cpp/wire_types.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/txn_header.h>
#include <lib/sync/completion.h>
#include <lib/zx/channel.h>
#include <zircon/assert.h>
#include <zircon/compiler.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <map>
#include <memory>
#include <thread>
#include <utility>
#include <fbl/ref_ptr.h>
#include <zxtest/zxtest.h>
#include "src/lib/storage/vfs/cpp/managed_vfs.h"
#include "src/lib/storage/vfs/cpp/synchronous_vfs.h"
#include "src/lib/storage/vfs/cpp/vfs.h"
#include "src/lib/storage/vfs/cpp/vfs_types.h"
#include "src/lib/storage/vfs/cpp/vnode.h"
namespace {
class FdCountVnode : public fs::Vnode {
public:
FdCountVnode() {}
virtual ~FdCountVnode() {
std::lock_guard lock(mutex_);
EXPECT_EQ(0, open_count());
}
int fds() const {
std::lock_guard lock(mutex_);
return open_count();
}
fs::VnodeProtocolSet GetProtocols() const final { return fs::VnodeProtocol::kFile; }
zx_status_t GetNodeInfoForProtocol([[maybe_unused]] fs::VnodeProtocol protocol,
[[maybe_unused]] fs::Rights rights,
fs::VnodeRepresentation* info) {
*info = fs::VnodeRepresentation::Connector();
return ZX_OK;
}
};
// TODO(fxbug.dev/42589): Clean up the array-of-completions pattern.
class AsyncTearDownVnode : public FdCountVnode {
public:
AsyncTearDownVnode(sync_completion_t* completions, zx_status_t status_for_sync = ZX_OK)
: callback_(nullptr), completions_(completions), status_for_sync_(status_for_sync) {}
~AsyncTearDownVnode() {
// C) Tear down the Vnode.
EXPECT_EQ(0, fds());
sync_completion_signal(&completions_[2]);
}
private:
void Sync(fs::Vnode::SyncCallback callback) final {
callback_ = std::move(callback);
std::thread thrd(&AsyncTearDownVnode::SyncThread, this);
thrd.detach();
}
static void SyncThread(AsyncTearDownVnode* arg) {
fs::Vnode::SyncCallback callback;
zx_status_t status_for_sync;
{
fbl::RefPtr<AsyncTearDownVnode> vn = fbl::RefPtr(arg);
status_for_sync = vn->status_for_sync_;
// A) Identify when the sync has started being processed.
sync_completion_signal(&vn->completions_[0]);
// B) Wait until the connection has been closed.
sync_completion_wait(&vn->completions_[1], ZX_TIME_INFINITE);
callback = std::move(vn->callback_);
}
callback(status_for_sync);
}
fs::Vnode::SyncCallback callback_;
sync_completion_t* completions_;
zx_status_t status_for_sync_;
};
void SendSync(fidl::UnownedClientEnd<fuchsia_io::Node> client) {
FIDL_ALIGNDECL
fidl::WireRequest<fuchsia_io::Node::Sync> request;
fidl::OwnedEncodedMessage<fidl::WireRequest<fuchsia_io::Node::Sync>> encoded(&request);
ASSERT_OK(encoded.status());
encoded.GetOutgoingMessage().set_txid(5);
encoded.Write(zx::unowned_channel(client.handle()));
ASSERT_OK(encoded.status());
}
// Helper function which creates a VFS with a served Vnode, starts a sync request, and then closes
// the connection to the client in the middle of the async callback.
//
// This helps tests get ready to try handling a tricky teardown.
void SyncStart(sync_completion_t* completions, async::Loop* loop,
std::unique_ptr<fs::ManagedVfs>* vfs, zx_status_t status_for_sync = ZX_OK) {
*vfs = std::make_unique<fs::ManagedVfs>(loop->dispatcher());
ASSERT_OK(loop->StartThread());
auto vn = fbl::AdoptRef(new AsyncTearDownVnode(completions, status_for_sync));
zx::status endpoints = fidl::CreateEndpoints<fuchsia_io::Node>();
ASSERT_OK(endpoints.status_value());
auto [client, server] = std::move(*endpoints);
auto validated_options = vn->ValidateOptions(fs::VnodeConnectionOptions());
ASSERT_TRUE(validated_options.is_ok());
ASSERT_OK(vn->Open(validated_options.value(), nullptr));
ASSERT_OK((*vfs)->Serve(vn, server.TakeChannel(), validated_options.value()));
vn = nullptr;
ASSERT_NO_FAILURES(SendSync(client));
// A) Wait for sync to begin.
sync_completion_wait(&completions[0], ZX_TIME_INFINITE);
client.reset();
}
void CommonTestUnpostedTeardown(zx_status_t status_for_sync) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sync_completion_t completions[3];
std::unique_ptr<fs::ManagedVfs> vfs;
ASSERT_NO_FAILURES(SyncStart(completions, &loop, &vfs, status_for_sync));
// B) Let sync complete.
sync_completion_signal(&completions[1]);
sync_completion_t* vnode_destroyed = &completions[2];
sync_completion_t shutdown_done;
vfs->Shutdown([&vnode_destroyed, &shutdown_done](zx_status_t status) {
ASSERT_OK(status);
// C) Issue an explicit shutdown, check that the Vnode has
// already torn down.
ASSERT_OK(sync_completion_wait(vnode_destroyed, ZX_SEC(0)));
sync_completion_signal(&shutdown_done);
});
ASSERT_OK(sync_completion_wait(&shutdown_done, ZX_SEC(3)));
}
// Test a case where the VFS object is shut down outside the dispatch loop.
TEST(Teardown, UnpostedTeardown) { CommonTestUnpostedTeardown(ZX_OK); }
// Test a case where the VFS object is shut down outside the dispatch loop, where the |Vnode::Sync|
// operation also failed causing the connection to be closed.
TEST(Teardown, UnpostedTeardownSyncError) { CommonTestUnpostedTeardown(ZX_ERR_INVALID_ARGS); }
void CommonTestPostedTeardown(zx_status_t status_for_sync) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sync_completion_t completions[3];
std::unique_ptr<fs::ManagedVfs> vfs;
ASSERT_NO_FAILURES(SyncStart(completions, &loop, &vfs, status_for_sync));
// B) Let sync complete.
sync_completion_signal(&completions[1]);
sync_completion_t* vnode_destroyed = &completions[2];
sync_completion_t shutdown_done;
ASSERT_OK(async::PostTask(loop.dispatcher(), [&]() {
vfs->Shutdown([&vnode_destroyed, &shutdown_done](zx_status_t status) {
ASSERT_OK(status);
// C) Issue an explicit shutdown, check that the Vnode has
// already torn down.
ASSERT_OK(sync_completion_wait(vnode_destroyed, ZX_SEC(0)));
sync_completion_signal(&shutdown_done);
});
}));
ASSERT_OK(sync_completion_wait(&shutdown_done, ZX_SEC(3)));
}
// Test a case where the VFS object is shut down as a posted request to the dispatch loop.
TEST(Teardown, PostedTeardown) { ASSERT_NO_FAILURES(CommonTestPostedTeardown(ZX_OK)); }
// Test a case where the VFS object is shut down as a posted request to the dispatch loop, where the
// |Vnode::Sync| operation also failed causing the connection to be closed.
TEST(Teardown, PostedTeardownSyncError) {
ASSERT_NO_FAILURES(CommonTestPostedTeardown(ZX_ERR_INVALID_ARGS));
}
// Test a case where the VFS object destroyed inside the callback to Shutdown.
TEST(Teardown, TeardownDeleteThis) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sync_completion_t completions[3];
std::unique_ptr<fs::ManagedVfs> vfs;
ASSERT_NO_FAILURES(SyncStart(completions, &loop, &vfs));
// B) Let sync complete.
sync_completion_signal(&completions[1]);
sync_completion_t* vnode_destroyed = &completions[2];
sync_completion_t shutdown_done;
fs::ManagedVfs* raw_vfs = vfs.release();
raw_vfs->Shutdown([&raw_vfs, &vnode_destroyed, &shutdown_done](zx_status_t status) {
ZX_ASSERT(status == ZX_OK);
// C) Issue an explicit shutdown, check that the Vnode has already torn down.
ZX_ASSERT(sync_completion_wait(vnode_destroyed, ZX_SEC(0)) == ZX_OK);
delete raw_vfs;
sync_completion_signal(&shutdown_done);
});
ASSERT_OK(sync_completion_wait(&shutdown_done, ZX_SEC(3)));
}
// Test a case where the VFS object is shut down before a background async callback gets the chance
// to complete.
TEST(Teardown, TeardownSlowAsyncCallback) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sync_completion_t completions[3];
std::unique_ptr<fs::ManagedVfs> vfs;
ASSERT_NO_FAILURES(SyncStart(completions, &loop, &vfs));
sync_completion_t* vnode_destroyed = &completions[2];
sync_completion_t shutdown_done;
vfs->Shutdown([&vnode_destroyed, &shutdown_done](zx_status_t status) {
ZX_ASSERT(status == ZX_OK);
// C) Issue an explicit shutdown, check that the Vnode has already torn down.
//
// Note: Will not be invoked until (B) completes.
ZX_ASSERT(sync_completion_wait(vnode_destroyed, ZX_SEC(0)) == ZX_OK);
sync_completion_signal(&shutdown_done);
});
// Shutdown should be waiting for our sync to finish.
ASSERT_EQ(ZX_ERR_TIMED_OUT, sync_completion_wait(&shutdown_done, ZX_MSEC(10)));
// B) Let sync complete.
sync_completion_signal(&completions[1]);
ASSERT_OK(sync_completion_wait(&shutdown_done, ZX_SEC(3)));
}
// Test a case where the VFS object is shut down while a clone request is concurrently trying to
// open a new connection.
TEST(Teardown, TeardownSlowClone) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sync_completion_t completions[3];
auto vfs = std::make_unique<fs::ManagedVfs>(loop.dispatcher());
ASSERT_OK(loop.StartThread());
auto vn = fbl::AdoptRef(new AsyncTearDownVnode(completions));
zx::status endpoints = fidl::CreateEndpoints<fuchsia_io::Node>();
ASSERT_OK(endpoints.status_value());
auto [client, server] = std::move(*endpoints);
auto validated_options = vn->ValidateOptions(fs::VnodeConnectionOptions());
ASSERT_TRUE(validated_options.is_ok());
ASSERT_OK(vn->Open(validated_options.value(), nullptr));
ASSERT_OK(vfs->Serve(vn, server.TakeChannel(), validated_options.value()));
vn = nullptr;
// A) Wait for sync to begin. Block the connection to the server in a sync, while simultaneously
// sending a request to open a new connection.
SendSync(client);
sync_completion_wait(&completions[0], ZX_TIME_INFINITE);
zx::status endpoints2 = fidl::CreateEndpoints<fuchsia_io::Node>();
ASSERT_OK(endpoints2.status_value());
fidl::WireSyncClient fidl_client2 = fidl::BindSyncClient(std::move(endpoints2->client));
ASSERT_OK(fidl_client2->Clone(0, std::move(endpoints2->server)).status());
// The connection is now:
// - In a sync callback,
// - Enqueued with a clone request,
// - Closed.
client.reset();
sync_completion_t* vnode_destroyed = &completions[2];
sync_completion_t shutdown_done;
vfs->Shutdown([&vnode_destroyed, &shutdown_done](zx_status_t status) {
ZX_ASSERT(status == ZX_OK);
// C) Issue an explicit shutdown, check that the Vnode has already torn down.
//
// Note: Will not be invoked until (B) completes.
ZX_ASSERT(sync_completion_wait(vnode_destroyed, ZX_SEC(0)) == ZX_OK);
sync_completion_signal(&shutdown_done);
});
// Shutdown should be waiting for our sync to finish.
ASSERT_EQ(ZX_ERR_TIMED_OUT, sync_completion_wait(&shutdown_done, ZX_MSEC(10)));
// B) Let sync complete. This should result in a successful termination of the filesystem, even
// with the pending clone request.
sync_completion_signal(&completions[1]);
ASSERT_OK(sync_completion_wait(&shutdown_done, ZX_SEC(3)));
}
TEST(Teardown, SynchronousTeardown) {
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
ASSERT_OK(loop.StartThread());
zx::channel client;
{
// Tear down the VFS while the async loop is running.
auto vfs = std::make_unique<fs::SynchronousVfs>(loop.dispatcher());
auto vn = fbl::AdoptRef(new FdCountVnode());
zx::channel server;
ASSERT_OK(zx::channel::create(0, &client, &server));
auto validated_options = vn->ValidateOptions(fs::VnodeConnectionOptions());
ASSERT_TRUE(validated_options.is_ok());
ASSERT_OK(vn->Open(validated_options.value(), nullptr));
ASSERT_OK(vfs->Serve(vn, std::move(server), validated_options.value()));
}
loop.Quit();
{
// Tear down the VFS while the async loop is not running.
auto vfs = std::make_unique<fs::SynchronousVfs>(loop.dispatcher());
auto vn = fbl::AdoptRef(new FdCountVnode());
zx::channel server;
ASSERT_OK(zx::channel::create(0, &client, &server));
auto validated_options = vn->ValidateOptions(fs::VnodeConnectionOptions());
ASSERT_TRUE(validated_options.is_ok());
ASSERT_OK(vn->Open(validated_options.value(), nullptr));
ASSERT_OK(vfs->Serve(vn, std::move(server), validated_options.value()));
}
{
// Tear down the VFS with no active connections.
auto vfs = std::make_unique<fs::SynchronousVfs>(loop.dispatcher());
}
}
class FakeDirectoryImpl : public fuchsia_io_admin::testing::DirectoryAdmin_TestBase {
public:
explicit FakeDirectoryImpl(zx_status_t unmount_status) : unmount_status_(unmount_status) {}
void NotImplemented_(const std::string& name, fidl::CompleterBase& completer) override {
ZX_ASSERT_MSG(false, "Unexpected call: %s", name.c_str());
completer.Close(ZX_ERR_NOT_SUPPORTED);
}
void Unmount(UnmountRequestView _request, UnmountCompleter::Sync& completer) override {
completer.Reply(unmount_status_);
}
private:
zx_status_t unmount_status_;
};
class FakeVnodeDir : public fs::Vnode {
public:
explicit FakeVnodeDir(PlatformVfs* vfs) : Vnode(vfs) {}
zx_status_t Create(std::string_view name, uint32_t _mode, fbl::RefPtr<Vnode>* out) override {
std::lock_guard l(mutex_);
auto created = fbl::AdoptRef(new FakeVnodeDir(vfs()));
*out = created;
children_[std::string(name)] = std::move(created);
return ZX_OK;
}
fs::VnodeProtocolSet GetProtocols() const override { return fs::VnodeProtocol::kDirectory; }
zx_status_t GetNodeInfoForProtocol(fs::VnodeProtocol _protocol, fs::Rights _rights,
fs::VnodeRepresentation* info) override {
*info = fs::VnodeRepresentation::Directory();
return ZX_OK;
}
zx_status_t Lookup(std::string_view name, fbl::RefPtr<Vnode>* out) override {
std::lock_guard l(mutex_);
auto location = children_.find(std::string(name));
if (location == children_.end()) {
return ZX_ERR_NOT_FOUND;
}
*out = location->second;
return ZX_OK;
}
zx_status_t AttachRemote(fs::MountChannel h) override {
std::lock_guard l(remote_lock_);
if (IsRemote()) {
return ZX_ERR_ALREADY_BOUND;
}
SetRemote(std::move(h.client_end()));
return ZX_OK;
}
bool IsRemote() const override __TA_REQUIRES(remote_lock_) { return remote_.is_valid(); }
fidl::ClientEnd<fuchsia_io::Directory> DetachRemote() override {
std::lock_guard l(remote_lock_);
return std::move(remote_);
}
fidl::UnownedClientEnd<fuchsia_io::Directory> GetRemote() const override {
std::lock_guard l(remote_lock_);
return remote_.borrow();
}
void SetRemote(fidl::ClientEnd<fuchsia_io::Directory> remote)
__TA_REQUIRES(remote_lock_) override {
remote_ = std::move(remote);
}
private:
mutable std::mutex remote_lock_;
std::map<std::string, fbl::RefPtr<Vnode>> children_ __TA_GUARDED(mutex_);
fidl::ClientEnd<fuchsia_io::Directory> remote_ __TA_GUARDED(remote_lock_);
};
void NestedFilesystemUnmount(zx_status_t on_remote_unmount) {
// Create outer filesystem
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
std::unique_ptr<fs::ManagedVfs> vfs = std::make_unique<fs::ManagedVfs>(loop.dispatcher());
ASSERT_OK(loop.StartThread());
auto vn = fbl::AdoptRef(new FakeVnodeDir(vfs.get()));
zx::channel client, server;
ASSERT_OK(zx::channel::create(0, &client, &server));
auto validated_options = vn->ValidateOptions(fs::VnodeConnectionOptions());
ASSERT_TRUE(validated_options.is_ok());
ASSERT_OK(vn->Open(validated_options.value(), nullptr));
ASSERT_OK(vfs->Serve(vn, std::move(server), validated_options.value()));
// Set up remote filesystem
async::Loop remote_loop(&kAsyncLoopConfigNoAttachToCurrentThread);
remote_loop.StartThread("remote-filesystem-loop");
zx::status create_endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
ASSERT_OK(create_endpoints.status_value());
auto [remote_client_end, remote_server_end] = std::move(create_endpoints.value());
fidl::BindServer(
remote_loop.dispatcher(),
fidl::ServerEnd<fuchsia_io_admin::DirectoryAdmin>(remote_server_end.TakeChannel()),
std::make_unique<FakeDirectoryImpl>(on_remote_unmount));
// Attach the remote
ASSERT_OK(
vfs->MountMkdir(vn, "foo", fs::MountChannel(std::move(remote_client_end)),
fuchsia_io::wire::kOpenRightReadable | fuchsia_io::wire::kOpenRightAdmin));
sync_completion_t shutdown_done;
zx_status_t result;
ASSERT_OK(async::PostTask(loop.dispatcher(), [&]() {
vfs->Shutdown([&shutdown_done, &result](zx_status_t status) {
result = status;
sync_completion_signal(&shutdown_done);
});
}));
ASSERT_OK(sync_completion_wait(&shutdown_done, ZX_SEC(3)));
ASSERT_EQ(result, on_remote_unmount);
}
TEST(Teardown, NestedFilesystemCleanUnmount) { ASSERT_NO_FAILURES(NestedFilesystemUnmount(ZX_OK)); }
TEST(Teardown, NestedFilesystemFailUnmount) {
ASSERT_NO_FAILURES(NestedFilesystemUnmount(ZX_ERR_ACCESS_DENIED));
}
} // namespace