| // 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 <fcntl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <limits.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #include <fbl/unique_fd.h> |
| #include <gmock/gmock.h> |
| |
| #include "guest_test.h" |
| #include "src/lib/fxl/arraysize.h" |
| #include "src/lib/fxl/logging.h" |
| #include "src/lib/fxl/strings/string_printf.h" |
| #include "src/virtualization/bin/vmm/device/block.h" |
| #include "src/virtualization/bin/vmm/device/qcow.h" |
| #include "src/virtualization/bin/vmm/device/qcow_test_data.h" |
| #include "src/virtualization/bin/vmm/guest_config.h" |
| |
| using namespace qcow_test_data; |
| using testing::HasSubstr; |
| |
| static constexpr char kVirtioBlockUtil[] = "virtio_block_test_util"; |
| static constexpr uint32_t kVirtioBlockCount = 32; |
| static constexpr uint32_t kVirtioQcowBlockCount = kDefaultHeaderV2.size / kBlockSectorSize; |
| |
| // We test reading and writing at the first and last offsets of the block device, and an arbitrary |
| // offset in between. |
| static constexpr off_t kMiddleOffset = 17; |
| static constexpr off_t kBlockTestOffsets[] = {0, kMiddleOffset, kVirtioBlockCount - 1}; |
| static constexpr off_t kQcowBlockTestOffsets[] = {0, kMiddleOffset, |
| (kClusterSize / kBlockSectorSize) - 1}; |
| |
| // Ensure that the offset we chose is less than the last offset. |
| static_assert(kMiddleOffset < kVirtioBlockCount - 1, "Virtio raw test offset is too large."); |
| static_assert(kMiddleOffset < (kClusterSize / kBlockSectorSize) - 1, |
| "Virtio qcow test offset is too large."); |
| |
| static constexpr off_t kQcowUnmappedClusterOffset = ClusterOffset(2) / kBlockSectorSize; |
| |
| static zx_status_t write_raw_file(int fd) { |
| int ret = ftruncate(fd, kVirtioBlockCount * kBlockSectorSize); |
| if (ret != 0) { |
| return ZX_ERR_IO; |
| } |
| return ZX_OK; |
| } |
| |
| template <typename T> |
| static bool write_at(int fd, const T* ptr, off_t off) { |
| ssize_t written = pwrite(fd, ptr, sizeof(T), off); |
| return written == static_cast<ssize_t>(sizeof(T)); |
| } |
| |
| // Writes an array of T values at the current file location. |
| template <typename T> |
| static bool write_at(int fd, const T* ptr, size_t len, off_t off) { |
| ssize_t written = pwrite(fd, ptr, len * sizeof(T), off); |
| return written == static_cast<ssize_t>(len * sizeof(T)); |
| } |
| |
| static zx_status_t write_qcow_file(int fd) { |
| QcowHeader header = kDefaultHeaderV2.HostToBigEndian(); |
| bool write_success = write_at(fd, &header, 0); |
| if (!write_success) { |
| return ZX_ERR_IO; |
| } |
| |
| // Convert L1 entries to big-endian |
| uint64_t be_table[arraysize(kL2TableClusterOffsets)]; |
| for (size_t i = 0; i < arraysize(kL2TableClusterOffsets); ++i) { |
| be_table[i] = HostToBigEndianTraits::Convert(kL2TableClusterOffsets[i]); |
| } |
| |
| // Write L1 table. |
| write_success = |
| write_at(fd, be_table, arraysize(kL2TableClusterOffsets), kDefaultHeaderV2.l1_table_offset); |
| if (!write_success) { |
| return ZX_ERR_IO; |
| } |
| |
| // Initialize empty L2 tables. |
| for (size_t i = 0; i < arraysize(kL2TableClusterOffsets); ++i) { |
| write_success = write_at(fd, kZeroCluster, sizeof(kZeroCluster), kL2TableClusterOffsets[i]); |
| if (!write_success) { |
| return ZX_ERR_IO; |
| } |
| } |
| |
| // Write L2 entry |
| uint64_t l2_offset = kL2TableClusterOffsets[0]; |
| uint64_t data_cluster_offset = ClusterOffset(kFirstDataCluster); |
| uint64_t l2_entry = HostToBigEndianTraits::Convert(data_cluster_offset); |
| write_success = write_at(fd, &l2_entry, l2_offset); |
| if (!write_success) { |
| return ZX_ERR_IO; |
| } |
| |
| // Write data to cluster. |
| uint8_t cluster_data[kClusterSize]; |
| memset(cluster_data, 0xab, sizeof(cluster_data)); |
| write_success = write_at(fd, cluster_data, kClusterSize, data_cluster_offset); |
| if (!write_success) { |
| return ZX_ERR_IO; |
| } |
| |
| return ZX_OK; |
| } |
| |
| struct TestDevice { |
| std::string id; |
| fuchsia::virtualization::BlockFormat format; |
| fuchsia::virtualization::BlockMode mode; |
| uint8_t pci_bus; |
| uint8_t pci_device; |
| std::string file_path = "/tmp/guest-test.XXXXXX"; |
| }; |
| |
| static zx_status_t create_test_device(TestDevice* test_device, |
| fuchsia::virtualization::BlockDevice* block_device) { |
| fbl::unique_fd fd(mkstemp(test_device->file_path.data())); |
| if (!fd) { |
| FXL_LOG(ERROR) << "Failed to create temporary file"; |
| return ZX_ERR_IO; |
| } |
| |
| zx_status_t status = ZX_ERR_BAD_STATE; |
| if (test_device->format == fuchsia::virtualization::BlockFormat::RAW) { |
| status = write_raw_file(fd.get()); |
| } else if (test_device->format == fuchsia::virtualization::BlockFormat::QCOW) { |
| status = write_qcow_file(fd.get()); |
| } |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| zx::channel channel; |
| status = fdio_get_service_handle(fd.release(), channel.reset_and_get_address()); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| block_device->id = test_device->id; |
| block_device->format = test_device->format; |
| block_device->mode = test_device->mode; |
| block_device->file = fidl::InterfaceHandle<fuchsia::io::File>(std::move(channel)).Bind(); |
| |
| return ZX_OK; |
| } |
| |
| class VirtioBlockTestGuest { |
| public: |
| zx_status_t CreateBlockDevices(uint8_t device_count, |
| fuchsia::virtualization::LaunchInfo* launch_info) { |
| zx_status_t status; |
| |
| test_devices_.push_back({ |
| .id = "raw_read_write", |
| .format = fuchsia::virtualization::BlockFormat::RAW, |
| .mode = fuchsia::virtualization::BlockMode::READ_WRITE, |
| .pci_bus = 0, |
| .pci_device = device_count++, |
| }); |
| test_devices_.push_back({ |
| .id = "raw_read_only", |
| .format = fuchsia::virtualization::BlockFormat::RAW, |
| .mode = fuchsia::virtualization::BlockMode::READ_ONLY, |
| .pci_bus = 0, |
| .pci_device = device_count++, |
| }); |
| test_devices_.push_back({ |
| .id = "raw_volatile_write", |
| .format = fuchsia::virtualization::BlockFormat::RAW, |
| .mode = fuchsia::virtualization::BlockMode::VOLATILE_WRITE, |
| .pci_bus = 0, |
| .pci_device = device_count++, |
| }); |
| test_devices_.push_back({ |
| .id = "qcow_read_only", |
| .format = fuchsia::virtualization::BlockFormat::QCOW, |
| .mode = fuchsia::virtualization::BlockMode::READ_ONLY, |
| .pci_bus = 0, |
| .pci_device = device_count++, |
| }); |
| test_devices_.push_back({ |
| .id = "qcow_volatile_write", |
| .format = fuchsia::virtualization::BlockFormat::QCOW, |
| .mode = fuchsia::virtualization::BlockMode::VOLATILE_WRITE, |
| .pci_bus = 0, |
| .pci_device = device_count++, |
| }); |
| |
| std::vector<fuchsia::virtualization::BlockDevice> block_devices; |
| for (auto& test_device : test_devices_) { |
| fuchsia::virtualization::BlockDevice block_device; |
| status = create_test_device(&test_device, &block_device); |
| if (status != ZX_OK) { |
| return status; |
| } |
| block_devices.push_back(std::move(block_device)); |
| } |
| |
| launch_info->block_devices = std::move(block_devices); |
| |
| return ZX_OK; |
| } |
| |
| const std::vector<TestDevice>& TestDevices() const { return test_devices_; } |
| |
| private: |
| std::vector<TestDevice> test_devices_; |
| }; |
| |
| class VirtioBlockZirconGuest : public ZirconEnclosedGuest, public VirtioBlockTestGuest { |
| public: |
| zx_status_t LaunchInfo(fuchsia::virtualization::LaunchInfo* launch_info) override { |
| zx_status_t status = ZirconEnclosedGuest::LaunchInfo(launch_info); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // Disable other virtio devices to ensure there's enough space on the PCI bus, and to simplify |
| // slot assignment. |
| launch_info->guest_config.set_default_net(false); |
| launch_info->guest_config.set_virtio_balloon(false); |
| launch_info->guest_config.set_virtio_gpu(false); |
| launch_info->guest_config.set_virtio_magma(false); |
| launch_info->guest_config.set_virtio_rng(false); |
| launch_info->guest_config.set_virtio_vsock(false); |
| guest_config::SetDefaults(&launch_info->guest_config); |
| |
| // Device count starts at 2: root device, block-0, then the test devices. |
| return CreateBlockDevices(/*device_count=*/2, launch_info); |
| } |
| }; |
| |
| class VirtioBlockDebianGuest : public DebianEnclosedGuest, public VirtioBlockTestGuest { |
| public: |
| zx_status_t LaunchInfo(fuchsia::virtualization::LaunchInfo* launch_info) override { |
| zx_status_t status = DebianEnclosedGuest::LaunchInfo(launch_info); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // Disable other virtio devices to ensure there's enough space on the PCI bus, and to simplify |
| // slot assignment. |
| launch_info->guest_config.set_default_net(false); |
| launch_info->guest_config.set_virtio_balloon(false); |
| launch_info->guest_config.set_virtio_gpu(false); |
| launch_info->guest_config.set_virtio_magma(false); |
| launch_info->guest_config.set_virtio_rng(false); |
| launch_info->guest_config.set_virtio_vsock(false); |
| guest_config::SetDefaults(&launch_info->guest_config); |
| |
| // Device count starts at 4: root device, block-0, block-1, block-2, then the test devices. |
| return CreateBlockDevices(/*device_count=*/4, launch_info); |
| } |
| }; |
| |
| template <class T> |
| class VirtioBlockGuestTest : public GuestTest<T> { |
| public: |
| const std::vector<TestDevice>& TestDevices() const { |
| return this->GetEnclosedGuest()->TestDevices(); |
| } |
| }; |
| |
| // The Debian tests are temporarily disabled until the test utilities roll. |
| using GuestTypes = ::testing::Types<VirtioBlockZirconGuest, VirtioBlockDebianGuest>; |
| TYPED_TEST_SUITE(VirtioBlockGuestTest, GuestTypes); |
| |
| TYPED_TEST(VirtioBlockGuestTest, CheckSize) { |
| for (const auto& device : this->TestDevices()) { |
| FXL_LOG(INFO) << "Device: " << device.id; |
| size_t expected_size = 0; |
| switch (device.format) { |
| case fuchsia::virtualization::BlockFormat::RAW: |
| expected_size = kVirtioBlockCount; |
| break; |
| case fuchsia::virtualization::BlockFormat::QCOW: |
| expected_size = kVirtioQcowBlockCount; |
| break; |
| default: |
| break; |
| } |
| ASSERT_GT(expected_size, 0u); |
| |
| std::string result; |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "check", |
| fxl::StringPrintf("%d", static_cast<int>(expected_size)), |
| }, |
| &result), |
| ZX_OK); |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| } |
| |
| TYPED_TEST(VirtioBlockGuestTest, ReadRaw) { |
| for (const auto& device : this->TestDevices()) { |
| if (device.format != fuchsia::virtualization::BlockFormat::RAW) { |
| continue; |
| } |
| FXL_LOG(INFO) << "Device: " << device.id; |
| |
| fbl::unique_fd fd(open(device.file_path.c_str(), O_RDWR)); |
| ASSERT_TRUE(fd); |
| |
| uint8_t data[kBlockSectorSize]; |
| memset(data, 0xab, kBlockSectorSize); |
| for (off_t offset : kBlockTestOffsets) { |
| ASSERT_EQ(pwrite(fd.get(), &data, kBlockSectorSize, offset * kBlockSectorSize), |
| static_cast<ssize_t>(kBlockSectorSize)); |
| std::string result; |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "read", |
| fxl::StringPrintf("%d", static_cast<int>(offset)), |
| fxl::StringPrintf("%d", 0xab), |
| }, |
| &result), |
| ZX_OK); |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| } |
| } |
| |
| TYPED_TEST(VirtioBlockGuestTest, WriteRaw) { |
| for (const auto& device : this->TestDevices()) { |
| if (device.format != fuchsia::virtualization::BlockFormat::RAW) { |
| continue; |
| } |
| FXL_LOG(INFO) << "Device: " << device.id; |
| |
| fbl::unique_fd fd(open(device.file_path.c_str(), O_RDWR)); |
| ASSERT_TRUE(fd); |
| |
| uint8_t data[kBlockSectorSize]; |
| memset(data, 0, kBlockSectorSize); |
| for (off_t offset : kBlockTestOffsets) { |
| // Write the block to zero. |
| ASSERT_EQ(pwrite(fd.get(), &data, kBlockSectorSize, offset * kBlockSectorSize), |
| static_cast<ssize_t>(kBlockSectorSize)); |
| |
| // Tell the guest to write bytes to the block. |
| std::string result; |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "write", |
| fxl::StringPrintf("%d", static_cast<int>(offset)), |
| fxl::StringPrintf("%d", 0xab), |
| }, |
| &result), |
| ZX_OK); |
| |
| // TODO(MAC-234): The virtio-block driver on Zircon currently doesn't inform |
| // the rest of the system when the device is read only. |
| if (this->GetGuestKernel() == GuestKernel::LINUX && |
| device.mode == fuchsia::virtualization::BlockMode::READ_ONLY) { |
| EXPECT_THAT(result, HasSubstr("PermissionDenied")); |
| } else { |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| |
| int expected_guest_read, expected_host_read; |
| switch (device.mode) { |
| case fuchsia::virtualization::BlockMode::READ_ONLY: |
| expected_guest_read = 0; |
| expected_host_read = 0; |
| break; |
| case fuchsia::virtualization::BlockMode::READ_WRITE: |
| expected_guest_read = 0xab; |
| expected_host_read = 0xab; |
| break; |
| case fuchsia::virtualization::BlockMode::VOLATILE_WRITE: |
| expected_guest_read = 0xab; |
| expected_host_read = 0; |
| break; |
| } |
| |
| // Check the value when read from the guest. |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "read", |
| fxl::StringPrintf("%d", static_cast<int>(offset)), |
| fxl::StringPrintf("%d", expected_guest_read), |
| }, |
| &result), |
| ZX_OK); |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| |
| // Check the value when read from the host file. |
| ASSERT_EQ(pread(fd.get(), &data, kBlockSectorSize, offset * kBlockSectorSize), |
| static_cast<ssize_t>(kBlockSectorSize)); |
| for (off_t i = 0; i != kBlockSectorSize; ++i) { |
| EXPECT_EQ(data[i], expected_host_read); |
| } |
| } |
| } |
| } |
| |
| TYPED_TEST(VirtioBlockGuestTest, ReadMappedCluster) { |
| for (const auto& device : this->TestDevices()) { |
| if (device.format != fuchsia::virtualization::BlockFormat::QCOW) { |
| continue; |
| } |
| FXL_LOG(INFO) << "Device: " << device.id; |
| |
| for (off_t offset : kQcowBlockTestOffsets) { |
| std::string result; |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "read", |
| fxl::StringPrintf("%d", static_cast<int>(offset)), |
| fxl::StringPrintf("%d", 0xab), |
| }, |
| &result), |
| ZX_OK); |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| } |
| } |
| |
| TYPED_TEST(VirtioBlockGuestTest, ReadUnmappedCluster) { |
| for (const auto& device : this->TestDevices()) { |
| if (device.format != fuchsia::virtualization::BlockFormat::QCOW) { |
| continue; |
| } |
| FXL_LOG(INFO) << "Device: " << device.id; |
| |
| std::string result; |
| EXPECT_EQ( |
| this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "read", |
| fxl::StringPrintf("%d", static_cast<int>(kQcowUnmappedClusterOffset)), |
| fxl::StringPrintf("%d", 0), |
| }, |
| &result), |
| ZX_OK); |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| } |
| |
| TYPED_TEST(VirtioBlockGuestTest, WriteQcow) { |
| for (const auto& device : this->TestDevices()) { |
| if (device.format != fuchsia::virtualization::BlockFormat::QCOW) { |
| continue; |
| } |
| FXL_LOG(INFO) << "Device: " << device.id; |
| |
| for (off_t offset : kQcowBlockTestOffsets) { |
| std::string result; |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "write", |
| fxl::StringPrintf("%d", static_cast<int>(offset)), |
| fxl::StringPrintf("%d", 0xba), |
| }, |
| &result), |
| ZX_OK); |
| |
| // TODO(MAC-234): The virtio-block driver on Zircon currently doesn't inform |
| // the rest of the system when the device is read only. |
| if (this->GetGuestKernel() == GuestKernel::LINUX && |
| device.mode == fuchsia::virtualization::BlockMode::READ_ONLY) { |
| EXPECT_THAT(result, HasSubstr("PermissionDenied")); |
| } else { |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| |
| int expected_read; |
| switch (device.mode) { |
| case fuchsia::virtualization::BlockMode::READ_ONLY: |
| expected_read = 0xab; |
| break; |
| case fuchsia::virtualization::BlockMode::VOLATILE_WRITE: |
| expected_read = 0xba; |
| break; |
| case fuchsia::virtualization::BlockMode::READ_WRITE: |
| // READ_WRITE not supported for QCOW. |
| expected_read = -1; |
| break; |
| } |
| |
| EXPECT_EQ(this->RunUtil(kVirtioBlockUtil, |
| { |
| fxl::StringPrintf("%lu", kBlockSectorSize), |
| fxl::StringPrintf("%u", device.pci_bus), |
| fxl::StringPrintf("%u", device.pci_device), |
| "read", |
| fxl::StringPrintf("%d", static_cast<int>(offset)), |
| fxl::StringPrintf("%d", expected_read), |
| }, |
| &result), |
| ZX_OK); |
| EXPECT_THAT(result, HasSubstr("PASS")); |
| } |
| } |
| } |