| // Copyright 2022 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/bin/linux_runner/block_devices.h" |
| |
| #include <fcntl.h> |
| #include <fuchsia/hardware/block/partition/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/time.h> |
| |
| #include <fbl/unique_fd.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/files/directory.h" |
| #include "src/lib/files/path.h" |
| #include "src/lib/storage/block_client/cpp/remote_block_device.h" |
| #include "src/storage/testing/fvm.h" |
| #include "src/storage/testing/ram_disk.h" |
| |
| class BlockDevicesTest : public ::testing::Test { |
| public: |
| static constexpr int kBlockSize = 512; |
| static constexpr uint64_t kBlockCount = 16 * 1024 * 1024 / kBlockSize; |
| static constexpr size_t kFvmSliceSize = 32 * 1024; |
| |
| void SetUp() override { |
| // Create a ramdisk. We tag with with the FVM GUID so that our code can correctly locate |
| // the FVM volume manager on this partition. |
| storage::RamDisk::Options ramdisk_options{ |
| .type_guid = {GUID_FVM_VALUE}, |
| }; |
| auto ramdisk = storage::RamDisk::Create(kBlockSize, kBlockCount, ramdisk_options); |
| FX_CHECK(ramdisk.is_ok()); |
| ramdisk_ = std::move(ramdisk.value()); |
| } |
| |
| protected: |
| void InitializeFvm() { |
| auto fvm_path = storage::CreateFvmInstance(ramdisk_.path(), kFvmSliceSize); |
| FX_CHECK(fvm_path.is_ok()); |
| fvm_path_ = std::move(fvm_path.value()); |
| } |
| |
| void InitializeFvmWithGuestPartition(size_t partition_size) { |
| storage::FvmOptions options{.name = kGuestPartitionName, |
| .type = kGuestPartitionGuid, |
| .initial_fvm_slice_count = partition_size / kFvmSliceSize}; |
| auto fvm_path = storage::CreateFvmPartition(ramdisk_.path(), kFvmSliceSize, options); |
| FX_CHECK(fvm_path.is_ok()); |
| fvm_path_ = std::move(fvm_path.value()); |
| } |
| |
| zx::status<std::array<uint8_t, GPT_GUID_LEN>> ReadPartitionTypeGuid(const std::string& path) { |
| fuchsia::hardware::block::partition::PartitionSyncPtr partition; |
| zx_status_t status = |
| fdio_service_connect(path.c_str(), partition.NewRequest().TakeChannel().release()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to connect to '" << path; |
| return zx::error(status); |
| } |
| |
| zx_status_t guid_status; |
| std::unique_ptr<fuchsia::hardware::block::partition::GUID> guid; |
| status = partition->GetTypeGuid(&guid_status, &guid); |
| if (status != ZX_OK || guid_status != ZX_OK || !guid) { |
| return zx::error(ZX_ERR_NOT_FOUND); |
| } |
| return zx::ok(guid->value); |
| } |
| |
| std::optional<std::string> FindPartitionWithGuid(std::array<uint8_t, GPT_GUID_LEN> guid) { |
| std::vector<std::string> contents; |
| bool result = files::ReadDirContents("/dev/class/block", &contents); |
| FX_CHECK(result) << "Failed to read block device directory: " << std::strerror(errno); |
| for (const auto& entry : contents) { |
| auto path = files::JoinPath("/dev/class/block", entry); |
| auto result = ReadPartitionTypeGuid(path); |
| if (result.is_ok()) { |
| if (result.value() == guid) { |
| return {std::move(path)}; |
| } |
| } |
| } |
| return {}; |
| } |
| |
| struct VolumeInfo { |
| uint64_t size; |
| std::string partition_name; |
| }; |
| zx::status<VolumeInfo> QueryVolumeInfo(const std::string& path) { |
| fuchsia::hardware::block::partition::PartitionSyncPtr partition; |
| zx_status_t status = |
| fdio_service_connect(path.c_str(), partition.NewRequest().TakeChannel().release()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to connect to '" << path; |
| return zx::error(status); |
| } |
| |
| zx_status_t op_status; |
| fidl::StringPtr name; |
| status = partition->GetName(&op_status, &name); |
| if (status != ZX_OK) { |
| return zx::error(status); |
| } |
| if (op_status != ZX_OK) { |
| return zx::error(status); |
| } |
| |
| std::unique_ptr<fuchsia::hardware::block::BlockInfo> block_info; |
| status = partition->GetInfo(&op_status, &block_info); |
| if (status != ZX_OK) { |
| return zx::error(status); |
| } |
| if (op_status != ZX_OK) { |
| return zx::error(status); |
| } |
| |
| return zx::ok(VolumeInfo{ |
| .size = block_info->block_count * block_info->block_size, |
| .partition_name = *name, |
| }); |
| } |
| |
| void CheckSlice(const std::string& volume, size_t slice, uint8_t expected_value) { |
| uint8_t expected_data[kFvmSliceSize]; |
| memset(expected_data, expected_value, sizeof(expected_data)); |
| |
| fbl::unique_fd fd; |
| fd.reset(open(volume.c_str(), O_RDWR)); |
| FX_CHECK(fd.get() >= 0); |
| |
| uint8_t actual_data[kFvmSliceSize] = {}; |
| FX_CHECK(ZX_OK == block_client::SingleReadBytes(fd.get(), actual_data, sizeof(actual_data), |
| kFvmSliceSize * slice)); |
| for (size_t i = 0; i < kFvmSliceSize; ++i) { |
| FX_CHECK(actual_data[i] == expected_data[i]) |
| << "Mismatch at byte " << i << " in slice " << slice << ". Values 0x" << std::hex |
| << static_cast<int>(actual_data[i]) << " != 0x" << static_cast<int>(expected_data[i]) |
| << "."; |
| } |
| } |
| |
| private: |
| storage::RamDisk ramdisk_; |
| std::string fvm_path_; |
| }; |
| |
| TEST_F(BlockDevicesTest, SetupWithoutPartition) { |
| InitializeFvm(); |
| EXPECT_TRUE(FindPartitionWithGuid(GUID_FVM_VALUE)); |
| EXPECT_FALSE(FindPartitionWithGuid(kGuestPartitionGuid)); |
| } |
| |
| TEST_F(BlockDevicesTest, SetupWithPartition) { |
| InitializeFvmWithGuestPartition(kFvmSliceSize); |
| EXPECT_TRUE(FindPartitionWithGuid(GUID_FVM_VALUE)); |
| EXPECT_TRUE(FindPartitionWithGuid(kGuestPartitionGuid)); |
| } |
| |
| TEST_F(BlockDevicesTest, CreateFvmPartitionIfNonExistant) { |
| InitializeFvm(); |
| |
| // Get the block devices. This should create a guest partition that is 10 FVM slices. |
| auto result = GetBlockDevices(10 * kFvmSliceSize); |
| |
| // Expect the partition is created. |
| ASSERT_TRUE(result.is_ok()); |
| EXPECT_TRUE(FindPartitionWithGuid(GUID_FVM_VALUE)); |
| auto guest_partition = FindPartitionWithGuid(kGuestPartitionGuid); |
| EXPECT_TRUE(guest_partition); |
| |
| // Verify size/name |
| auto info = QueryVolumeInfo(*guest_partition); |
| EXPECT_TRUE(info.is_ok()); |
| EXPECT_EQ(info.value().partition_name, kGuestPartitionName); |
| EXPECT_EQ(info.value().size, 10 * kFvmSliceSize); |
| } |
| |
| TEST_F(BlockDevicesTest, ReuseExistingPartition) { |
| // Initialize a guest partition with a single FVM slice. |
| InitializeFvmWithGuestPartition(kFvmSliceSize); |
| |
| // Get block devices and request the partition to be 10 slices. This doesn't resize an existing |
| // partition so size parameter here is effectively ignored. |
| auto result = GetBlockDevices(10 * kFvmSliceSize); |
| |
| // Expect to find a partition with a single slice. |
| ASSERT_TRUE(result.is_ok()); |
| EXPECT_TRUE(FindPartitionWithGuid(GUID_FVM_VALUE)); |
| auto guest_partition = FindPartitionWithGuid(kGuestPartitionGuid); |
| EXPECT_TRUE(guest_partition); |
| |
| // Verify size/name |
| auto info = QueryVolumeInfo(*guest_partition); |
| EXPECT_TRUE(info.is_ok()); |
| EXPECT_EQ(info.value().partition_name, kGuestPartitionName); |
| EXPECT_EQ(info.value().size, kFvmSliceSize); |
| } |
| |
| TEST_F(BlockDevicesTest, WipeStatefulPartition) { |
| // Create a device with 10 slices. |
| InitializeFvmWithGuestPartition(10 * kFvmSliceSize); |
| auto guest_partition = FindPartitionWithGuid(kGuestPartitionGuid); |
| EXPECT_TRUE(guest_partition); |
| |
| // Fill the entire partition with one bit-pattern and then wipe the first half back to 0. |
| ASSERT_TRUE(WipeStatefulPartition(10 * kFvmSliceSize, 0xab).is_ok()); |
| ASSERT_TRUE(WipeStatefulPartition(5 * kFvmSliceSize).is_ok()); |
| |
| // Check the slices. These should be all 0. |
| for (size_t i = 0; i < 5; ++i) { |
| CheckSlice(*guest_partition, i, 0); |
| } |
| // The last 5 should still be 0xab. |
| for (size_t i = 5; i < 10; ++i) { |
| CheckSlice(*guest_partition, i, 0xab); |
| } |
| } |