blob: fc4b4f28804cceb9f0c49338144edbc80d890e21 [file] [log] [blame]
// Copyright 2019 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 "src/virtualization/packages/biscotti_guest/linux_runner/guest.h"
#include <errno.h>
#include <fcntl.h>
#include <fuchsia/virtualization/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fdio/namespace.h>
#include <lib/gtest/test_loop_fixture.h>
#include <lib/memfs/memfs.h>
#include <lib/sync/completion.h>
#include <lib/sys/cpp/testing/component_context_provider.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/virtualization/testing/fake_manager.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <unordered_map>
#include "src/virtualization/packages/biscotti_guest/linux_runner/ports.h"
namespace linux_runner {
namespace {
// Use a small image here since we won't actually put any data on it; we just
// want to verify we can correctly create the image.
static constexpr off_t kStatefulImageSizeForTest = 10ul * 1024 * 1024;
static constexpr const char* kStatefulImagePath = "/data/stateful.img";
// Mounts a memfs filesystem at a given path and unmounts it when this object
// goes out of scope.
class ScopedMemfs {
public:
// Creates a new memfs filesystem at the given path.
static zx_status_t InstallAt(const char* path, async_dispatcher_t* dispatcher,
std::unique_ptr<ScopedMemfs>* out) {
fdio_ns_t* ns;
zx_status_t status = fdio_ns_get_installed(&ns);
if (status != ZX_OK) {
return status;
}
memfs_filesystem_t* fs;
zx_handle_t root;
status = memfs_create_filesystem(dispatcher, &fs, &root);
if (status != ZX_OK) {
return status;
}
status = fdio_ns_bind(ns, path, root);
if (status != ZX_OK) {
memfs_free_filesystem(fs, nullptr);
return status;
}
*out = std::make_unique<ScopedMemfs>(fs, path);
return ZX_OK;
}
ScopedMemfs(memfs_filesystem_t* fs, const char* path) : fs_(fs), path_(path) {}
~ScopedMemfs() {
fdio_ns_t* ns;
sync_completion_t completion;
memfs_free_filesystem(fs_, &completion);
FX_CHECK(sync_completion_wait(&completion, ZX_TIME_INFINITE) == ZX_OK)
<< "Failed to unmount memfs";
FX_CHECK(fdio_ns_get_installed(&ns) == ZX_OK) << "Failed to read namespaces";
FX_CHECK(fdio_ns_unbind(ns, path_) == ZX_OK) << "Failed to unbind memfs filesystem";
}
private:
memfs_filesystem_t* fs_;
const char* path_;
};
class LinuxRunnerGuestTest : public gtest::TestLoopFixture {
public:
void SetUp() override {
TestLoopFixture::SetUp();
// Install memfs on a different async loop thread to resolve some deadlock
// when doing blocking file operations on our test loop.
FX_CHECK(ScopedMemfs::InstallAt("/data", memfs_loop_.dispatcher(), &data_) == ZX_OK);
memfs_loop_.StartThread();
// Add a fake guest Manager to the components context.
provider_.service_directory_provider()->AddService(fake_guest_manager_.GetHandler());
}
void TearDown() override {
TestLoopFixture::TearDown();
data_.reset();
memfs_loop_.Shutdown();
}
protected:
void StartGuest() {
GuestConfig config = {
.stateful_image_size = kStatefulImageSizeForTest,
};
Guest::CreateAndStart(provider_.context(), config, &guest_);
RunLoopUntilIdle();
}
guest::testing::FakeManager* guest_manager() { return &fake_guest_manager_; }
private:
guest::testing::FakeManager fake_guest_manager_;
std::unique_ptr<Guest> guest_;
sys::testing::ComponentContextProvider provider_;
async::Loop memfs_loop_{&kAsyncLoopConfigNoAttachToCurrentThread};
std::unique_ptr<ScopedMemfs> data_;
};
TEST_F(LinuxRunnerGuestTest, ConnectToStartupListener) {
StartGuest();
zx::handle handle;
zx_status_t status = guest_manager()->GuestVsock()->ConnectToHost(
kStartupListenerPort, [&handle](zx::handle h) { handle = std::move(h); });
ASSERT_EQ(ZX_OK, status) << "linux_runner is not listening on StartupListener port";
RunLoopUntilIdle();
// We've estabished a VSOCK connection to the host. This is how the guest
// signals boot completed.
ASSERT_TRUE(handle) << "Unable to connect to StartupListener";
}
// If a stateful image partition does not exist on device; one shall be created
// as part of the guest creation.
TEST_F(LinuxRunnerGuestTest, CreateEmptyStatefulPartition) {
// Verify no image exists.
struct stat st = {};
ASSERT_EQ(-1, stat(kStatefulImagePath, &st)) << "Stateful image already exists";
ASSERT_EQ(ENOENT, errno);
StartGuest();
// Verfify an image file has been created with the expected size:
ASSERT_EQ(0, stat(kStatefulImagePath, &st)) << "Stateful was not created";
ASSERT_TRUE(S_ISREG(st.st_mode));
ASSERT_EQ(st.st_size, kStatefulImageSizeForTest);
}
// TODO(fxbug.dev/40751): With ShadowCallStack enabled and SafeStack disabled, we can
// trigger a segfault in `ReuseExistingStatefulParition` all the way in
// pthread_mutex_lock. We believe the underlying cause of this is some race
// condition internal to gRPC. The segfault seems nondeterministic in that there
// are many ways to hide the segfault, including:
// - Disabling at least one of the other tests
// - Avoid reading the `handle` at the end of `ConnectToStartupListener`
// - Adding a log right after declaring the handle in
// `ConnectToStartupListener`
// - Moving the SetUp and TearDown logic to the start and end of each test
// function
// - Probably others to be discovered...
TEST_F(LinuxRunnerGuestTest, DISABLED_ReuseExistingStatefulParition) {
// Use a different size here to verify we don't go though the partition create
// logic, which will create a full-size image.
static constexpr auto image_size = 1024;
int fd = open(kStatefulImagePath, O_RDWR | O_CREAT);
ASSERT_GE(fd, 0);
// Write some data do the disk image;
uint8_t expected[image_size];
for (size_t i = 0; i < image_size; ++i) {
expected[i] = static_cast<uint8_t>(i & UINT8_MAX);
}
ASSERT_EQ(write(fd, expected, sizeof(expected)), static_cast<int>(sizeof(expected)))
<< "Failed to write test data to disk image";
close(fd);
StartGuest();
// Read disk back out and verify it has not been changed.
fd = open(kStatefulImagePath, O_RDONLY);
ASSERT_GE(fd, 0) << "Stateful has been deleted";
uint8_t actual[image_size];
static_assert(sizeof(actual) == sizeof(expected));
ASSERT_EQ(read(fd, actual, sizeof(actual)), static_cast<int>(sizeof(actual)))
<< "Failed to read back disk image";
ASSERT_EQ(0, memcmp(actual, expected, sizeof(actual))) << "Disk image has changed";
}
} // namespace
} // namespace linux_runner