| // 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 "block.h" |
| |
| #include <fidl/fuchsia.storage.block/cpp/fidl.h> |
| #include <lib/driver/component/cpp/driver_export2.h> |
| #include <lib/driver/logging/cpp/logger.h> |
| #include <lib/driver/testing/cpp/driver_test.h> |
| #include <lib/driver/testing/cpp/minimal_compat_environment.h> |
| #include <lib/fake-bti/bti.h> |
| #include <lib/sync/completion.h> |
| #include <lib/virtio/backends/fake.h> |
| |
| #include <condition_variable> |
| #include <cstdint> |
| #include <memory> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/testing/predicates/status.h" |
| #include "src/storage/lib/block_client/cpp/remote_block_device.h" |
| |
| namespace { |
| |
| constexpr uint64_t kCapacity = 200; |
| constexpr uint64_t kSizeMax = 4000; |
| constexpr uint64_t kSegMax = 1024; |
| constexpr uint64_t kBlkSize = 1024; |
| constexpr uint64_t kVmoOffsetBlocks = 1; |
| const uint16_t kRingSize = 128; // Should match block.h |
| |
| // Fake virtio 'backend' for a virtio-block device. |
| class FakeBackendForBlock : public virtio::FakeBackend { |
| public: |
| FakeBackendForBlock(zx_handle_t fake_bti) |
| : virtio::FakeBackend({{0, 1024}}), fake_bti_(fake_bti) { |
| // Fill out a block config: |
| virtio_blk_config config; |
| memset(&config, 0, sizeof(config)); |
| config.capacity = kCapacity; |
| config.size_max = kSizeMax; |
| config.seg_max = kSegMax; |
| config.blk_size = kBlkSize; |
| |
| for (uint16_t i = 0; i < sizeof(config); ++i) { |
| AddClassRegister(i, reinterpret_cast<uint8_t*>(&config)[i]); |
| } |
| } |
| |
| void set_status(uint8_t status) { status_ = status; } |
| |
| uint64_t ReadFeatures() override { |
| uint64_t bitmap = FakeBackend::ReadFeatures(); |
| |
| // Declare support for VIRTIO_F_VERSION_1. |
| bitmap |= VIRTIO_F_VERSION_1; |
| |
| return bitmap; |
| } |
| |
| void RingKick(uint16_t ring_index) override { |
| FakeBackend::RingKick(ring_index); |
| |
| fake_bti_pinned_vmo_info_t vmos[16]; |
| size_t count; |
| ASSERT_OK(fake_bti_get_pinned_vmos(fake_bti_, vmos, 16, &count)); |
| ASSERT_LE(size_t{2}, count); |
| |
| union __PACKED Used { |
| vring_used head; |
| struct __PACKED { |
| uint8_t header[sizeof(vring_used)]; |
| vring_used_elem elements[kRingSize]; |
| }; |
| } used; |
| union __PACKED Avail { |
| vring_avail head; |
| struct __PACKED { |
| uint8_t header[sizeof(vring_avail)]; |
| uint16_t ring[kRingSize]; |
| }; |
| } avail; |
| |
| // This assumes that the ring is in the first VMO. |
| ASSERT_OK(zx_vmo_read(vmos[0].vmo, &used, vmos[0].offset + used_offset_, sizeof(used))); |
| ASSERT_OK(zx_vmo_read(vmos[0].vmo, &avail, vmos[0].offset + avail_offset_, sizeof(avail))); |
| |
| if (avail.head.idx != used.head.idx) { |
| ASSERT_EQ(avail.head.idx, used.head.idx + 1); // We can only handle one queued entry. |
| |
| size_t index = used.head.idx & (kRingSize - 1); |
| |
| // Read the descriptors. |
| vring_desc descriptors[kRingSize]; |
| ASSERT_OK(zx_vmo_read(vmos[0].vmo, descriptors, vmos[0].offset + desc_offset_, |
| sizeof(descriptors))); |
| |
| // Find the last descriptor. |
| vring_desc* desc = &descriptors[avail.ring[index]]; |
| uint16_t count = 1; |
| uint16_t data_descriptor_idx = UINT16_MAX; |
| while (desc->flags & VRING_DESC_F_NEXT) { |
| if (desc->addr % zx_system_get_page_size() == kBlkSize * kVmoOffsetBlocks) { |
| last_data_offset_ = desc->addr - FAKE_BTI_PHYS_ADDR; |
| data_descriptor_idx = count; |
| } |
| desc = &descriptors[desc->next]; |
| ++count; |
| } |
| // The second-last descriptor describes the first page of data transfer (the first descriptor |
| // is the head descriptor). |
| if (data_descriptor_idx != UINT16_MAX) { |
| ZX_ASSERT_MSG(data_descriptor_idx == count - 1, |
| "The second-last descriptor should point to data"); |
| } |
| |
| // It should be the status descriptor. |
| ASSERT_EQ(uint32_t{1}, desc->len); |
| |
| // This assumes the results are in the second VMO. |
| size_t offset = vmos[1].offset + desc->addr - FAKE_BTI_PHYS_ADDR; |
| ASSERT_OK(zx_vmo_write(vmos[1].vmo, &status_, offset, sizeof(status_))); |
| |
| used.elements[index].id = avail.ring[index]; |
| used.elements[index].len = count; |
| |
| ++used.head.idx; |
| |
| ASSERT_OK(zx_vmo_write(vmos[0].vmo, &used, vmos[0].offset + used_offset_, sizeof(used))); |
| |
| // Trigger an interrupt. |
| uint8_t isr_status; |
| ReadRegister(kISRStatus, &isr_status); |
| isr_status |= VIRTIO_ISR_QUEUE_INT; |
| SetRegister(kISRStatus, isr_status); |
| |
| std::scoped_lock lock(mutex_); |
| interrupt_ = true; |
| cond_.notify_all(); |
| } |
| } |
| |
| zx_status_t SetRing(uint16_t index, uint16_t count, zx_paddr_t pa_desc, zx_paddr_t pa_avail, |
| zx_paddr_t pa_used) override { |
| FakeBackend::SetRing(index, count, pa_desc, pa_avail, pa_used); |
| used_offset_ = pa_used - FAKE_BTI_PHYS_ADDR; |
| avail_offset_ = pa_avail - FAKE_BTI_PHYS_ADDR; |
| desc_offset_ = pa_desc - FAKE_BTI_PHYS_ADDR; |
| ZX_ASSERT(count == kRingSize); |
| return ZX_OK; |
| } |
| |
| zx_status_t InterruptValid() override { |
| std::scoped_lock lock(mutex_); |
| return terminate_ ? ZX_ERR_CANCELED : ZX_OK; |
| } |
| |
| zx::result<uint32_t> WaitForInterrupt() override { |
| std::unique_lock<std::mutex> lock(mutex_); |
| for (;;) { |
| if (terminate_) |
| return zx::error(ZX_ERR_CANCELED); |
| if (interrupt_) |
| return zx::ok(0); |
| cond_.wait(lock); |
| } |
| } |
| |
| void InterruptAck(uint32_t key) override { |
| std::scoped_lock lock(mutex_); |
| interrupt_ = false; |
| } |
| |
| void Terminate() override { |
| std::scoped_lock lock(mutex_); |
| terminate_ = true; |
| cond_.notify_all(); |
| } |
| |
| // Used to peek at the data offset of the last submitted request. |
| static std::atomic<uint64_t> last_data_offset_; |
| |
| private: |
| // The vring offsets. |
| size_t used_offset_ = 0; |
| size_t avail_offset_ = 0; |
| size_t desc_offset_ = 0; |
| |
| zx_handle_t fake_bti_; |
| |
| std::mutex mutex_; |
| std::condition_variable cond_; |
| bool terminate_ = false; |
| bool interrupt_ = false; |
| |
| // The status returned for any operations. |
| uint8_t status_ = VIRTIO_BLK_S_OK; |
| }; |
| |
| class TestBlockDriver : public virtio::BlockDriver { |
| public: |
| // Modify to configure the behaviour of this test driver. |
| static uint8_t backend_status_; |
| |
| explicit TestBlockDriver() : BlockDriver() {} |
| |
| virtio::BlockDevice& block_device() const { return virtio::BlockDriver::block_device(); } |
| |
| protected: |
| zx::result<std::unique_ptr<virtio::BlockDevice>> CreateBlockDevice( |
| const fdf::Namespace& incoming) override { |
| zx::bti bti(ZX_HANDLE_INVALID); |
| zx_status_t status = fake_bti_create(bti.reset_and_get_address()); |
| if (status != ZX_OK) { |
| return zx::error(status); |
| } |
| auto backend = std::make_unique<FakeBackendForBlock>(bti.get()); |
| backend->set_status(backend_status_); |
| |
| return zx::ok( |
| std::make_unique<virtio::BlockDevice>(std::move(bti), std::move(backend), logger())); |
| } |
| }; |
| |
| uint8_t TestBlockDriver::backend_status_; |
| std::atomic<uint64_t> FakeBackendForBlock::last_data_offset_ = 0; |
| |
| class TestConfig final { |
| public: |
| using DriverType = TestBlockDriver; |
| using EnvironmentType = fdf_testing::MinimalCompatEnvironment; |
| }; |
| |
| // Provides control primitives for tests that issue IO requests to the device. |
| class BlockDriverTest : public ::testing::Test { |
| public: |
| void StartDriver(uint8_t status = VIRTIO_BLK_S_OK) { |
| zx::event token; |
| ASSERT_OK(zx::event::create(0, &token)); |
| StartDriverWithNodeToken(std::move(token), status); |
| } |
| void StartDriverWithNodeToken(zx::event node_token, uint8_t status = VIRTIO_BLK_S_OK) { |
| TestBlockDriver::backend_status_ = status; |
| zx::result<> result = driver_test().StartDriverWithCustomStartArgs( |
| [&](fdf::DriverStartArgs& args) { args.node_token(std::move(node_token)); }); |
| ASSERT_EQ(ZX_OK, result.status_value()); |
| } |
| void TearDown() override { |
| zx::result<> result = driver_test().StopDriver(); |
| ASSERT_EQ(ZX_OK, result.status_value()); |
| } |
| fdf_testing::BackgroundDriverTest<TestConfig>& driver_test() { return driver_test_; } |
| |
| zx::result<std::unique_ptr<block_client::RemoteBlockDevice>> CreateClient() { |
| auto [volume_client, volume_server] = fidl::Endpoints<fuchsia_storage_block::Block>::Create(); |
| driver_test().RunInDriverContext([&](TestBlockDriver& driver) { |
| driver.block_device().ServeRequests(std::move(volume_server)); |
| }); |
| return block_client::RemoteBlockDevice::Create(std::move(volume_client)); |
| } |
| |
| private: |
| fdf_testing::BackgroundDriverTest<TestConfig> driver_test_; |
| }; |
| |
| TEST_F(BlockDriverTest, QueueOne) { |
| StartDriver(); |
| |
| zx::result client = CreateClient(); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client.value()->BlockGetInfo(&info)); |
| const size_t kLen = info.max_transfer_size + kBlkSize; |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(kLen, 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client.value()->BlockAttachVmo(vmo, &owned_vmoid)); |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| BlockFifoRequest request = {.command = {.opcode = BLOCK_OPCODE_READ}, |
| .vmoid = vmoid, |
| .length = 0, |
| .vmo_offset = kVmoOffsetBlocks, |
| .dev_offset = 0}; |
| ASSERT_EQ(ZX_ERR_INVALID_ARGS, client.value()->FifoTransaction(&request, 1)); |
| |
| request.length = kCapacity + 1; |
| ASSERT_EQ(ZX_ERR_OUT_OF_RANGE, client.value()->FifoTransaction(&request, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, CheckQuery) { |
| StartDriver(); |
| |
| zx::result client = CreateClient(); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client.value()->BlockGetInfo(&info)); |
| ASSERT_EQ(info.block_size, kBlkSize); |
| ASSERT_EQ(info.block_count, kCapacity); |
| ASSERT_GE(info.max_transfer_size, zx_system_get_page_size()); |
| } |
| |
| TEST_F(BlockDriverTest, ReadOk) { |
| StartDriver(); |
| |
| zx::result client = CreateClient(); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client.value()->BlockGetInfo(&info)); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(zx_system_get_page_size(), 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client.value()->BlockAttachVmo(vmo, &owned_vmoid)); |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| BlockFifoRequest request = {.command = {.opcode = BLOCK_OPCODE_READ}, |
| .vmoid = vmoid, |
| .length = 1, |
| .vmo_offset = kVmoOffsetBlocks, |
| .dev_offset = 0}; |
| |
| EXPECT_OK(client.value()->FifoTransaction(&request, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, ReadError) { |
| StartDriver(VIRTIO_BLK_S_IOERR); |
| |
| zx::result client = CreateClient(); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client.value()->BlockGetInfo(&info)); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(zx_system_get_page_size(), 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client.value()->BlockAttachVmo(vmo, &owned_vmoid)); |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| BlockFifoRequest request = {.command = {.opcode = BLOCK_OPCODE_READ}, |
| .vmoid = vmoid, |
| .length = 1, |
| .vmo_offset = kVmoOffsetBlocks, |
| .dev_offset = 0}; |
| |
| ASSERT_EQ(ZX_ERR_IO, client.value()->FifoTransaction(&request, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, Trim) { |
| StartDriver(); |
| |
| zx::result client = CreateClient(); |
| ASSERT_OK(client); |
| |
| BlockFifoRequest request = {.command = {.opcode = BLOCK_OPCODE_TRIM}, |
| .vmoid = 0, |
| .length = 1, |
| .vmo_offset = 0, |
| .dev_offset = 0}; |
| |
| EXPECT_OK(client.value()->FifoTransaction(&request, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, BlockServer) { |
| StartDriver(); |
| |
| auto [volume_client, volume_server] = fidl::Endpoints<fuchsia_storage_block::Block>::Create(); |
| driver_test().RunInDriverContext([&](TestBlockDriver& driver) { |
| driver.block_device().ServeRequests(std::move(volume_server)); |
| }); |
| zx::result client = block_client::RemoteBlockDevice::Create(std::move(volume_client)); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client->BlockGetInfo(&info)); |
| const size_t kLen = info.max_transfer_size + kBlkSize; |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(kLen, 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client->BlockAttachVmo(vmo, &owned_vmoid)); |
| |
| // It doesn't matter if we leak the ID. |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| BlockFifoRequest requests[] = { |
| {.command = |
| { |
| .opcode = BLOCK_OPCODE_WRITE, |
| }, |
| .vmoid = vmoid, |
| .length = 3, |
| .vmo_offset = 0, |
| .dev_offset = 0}, |
| {.command = |
| { |
| .opcode = BLOCK_OPCODE_READ, |
| }, |
| .vmoid = vmoid, |
| .length = 3, |
| .vmo_offset = 10, |
| .dev_offset = 100}, |
| {.command = |
| { |
| .opcode = BLOCK_OPCODE_TRIM, |
| }, |
| .vmoid = 0, |
| .length = 3, |
| .vmo_offset = 0, |
| .dev_offset = 3}, |
| }; |
| |
| EXPECT_OK(client->FifoTransaction(requests, 3)); |
| |
| BlockFifoRequest big_request = {.command = |
| { |
| .opcode = BLOCK_OPCODE_WRITE, |
| }, |
| .vmoid = vmoid, |
| .length = static_cast<uint32_t>(kCapacity), |
| .vmo_offset = 0, |
| .dev_offset = 0}; |
| |
| EXPECT_OK(client->FifoTransaction(&big_request, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, BarriersOk) { |
| StartDriver(); |
| |
| auto [volume_client, volume_server] = fidl::Endpoints<fuchsia_storage_block::Block>::Create(); |
| driver_test().RunInDriverContext([&](TestBlockDriver& driver) { |
| driver.block_device().ServeRequests(std::move(volume_server)); |
| }); |
| zx::result client = block_client::RemoteBlockDevice::Create(std::move(volume_client)); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client->BlockGetInfo(&info)); |
| const size_t kLen = info.max_transfer_size + kBlkSize; |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(kLen, 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client->BlockAttachVmo(vmo, &owned_vmoid)); |
| |
| // It doesn't matter if we leak the ID. |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| BlockFifoRequest requests[] = { |
| {.command = |
| { |
| .opcode = BLOCK_OPCODE_WRITE, |
| .flags = BLOCK_IO_FLAG_PRE_BARRIER, |
| }, |
| .vmoid = vmoid, |
| .length = 3, |
| .vmo_offset = 0, |
| .dev_offset = 0}, |
| }; |
| |
| EXPECT_OK(client->FifoTransaction(requests, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, BarriersError) { |
| StartDriver(VIRTIO_BLK_S_IOERR); |
| |
| auto [volume_client, volume_server] = fidl::Endpoints<fuchsia_storage_block::Block>::Create(); |
| driver_test().RunInDriverContext([&](TestBlockDriver& driver) { |
| driver.block_device().ServeRequests(std::move(volume_server)); |
| }); |
| zx::result client = block_client::RemoteBlockDevice::Create(std::move(volume_client)); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client->BlockGetInfo(&info)); |
| const size_t kLen = info.max_transfer_size + kBlkSize; |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(kLen, 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client->BlockAttachVmo(vmo, &owned_vmoid)); |
| |
| // It doesn't matter if we leak the ID. |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| BlockFifoRequest requests[] = { |
| {.command = |
| { |
| .opcode = BLOCK_OPCODE_WRITE, |
| .flags = BLOCK_IO_FLAG_PRE_BARRIER, |
| }, |
| .vmoid = vmoid, |
| .length = 3, |
| .vmo_offset = 0, |
| .dev_offset = 0}, |
| }; |
| |
| ASSERT_EQ(ZX_ERR_IO, client->FifoTransaction(requests, 1)); |
| } |
| |
| TEST_F(BlockDriverTest, NodeToken) { |
| zx::event token; |
| ASSERT_OK(zx::event::create(0, &token)); |
| zx::event token_copy; |
| ASSERT_OK(token.duplicate(ZX_RIGHT_SAME_RIGHTS, &token_copy)); |
| |
| StartDriverWithNodeToken(std::move(token)); |
| |
| zx::result connect_result = |
| driver_test().Connect<fuchsia_hardware_block_volume::Service::Token>(); |
| ASSERT_OK(connect_result); |
| |
| fidl::SyncClient<fuchsia_driver_token::NodeToken> client(std::move(connect_result.value())); |
| |
| auto get_result = client->Get(); |
| |
| ASSERT_OK(get_result); |
| |
| zx_info_handle_basic_t info1, info2; |
| ASSERT_EQ(token_copy.get_info(ZX_INFO_HANDLE_BASIC, &info1, sizeof(info1), nullptr, nullptr), |
| ZX_OK); |
| ASSERT_EQ( |
| get_result->token().get_info(ZX_INFO_HANDLE_BASIC, &info2, sizeof(info2), nullptr, nullptr), |
| ZX_OK); |
| ASSERT_EQ(info1.koid, info2.koid); |
| } |
| |
| TEST_F(BlockDriverTest, UnalignedVmoOffset) { |
| StartDriver(); |
| |
| zx::result client = CreateClient(); |
| ASSERT_OK(client); |
| |
| fuchsia_storage_block::wire::BlockInfo info; |
| ASSERT_OK(client.value()->BlockGetInfo(&info)); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(static_cast<uint64_t>(zx_system_get_page_size()) * 2, 0, &vmo)); |
| |
| storage::Vmoid owned_vmoid; |
| EXPECT_OK(client.value()->BlockAttachVmo(vmo, &owned_vmoid)); |
| vmoid_t vmoid = owned_vmoid.TakeId(); |
| |
| // Use an offset of 1 block. With kBlkSize=1024, this translates to 1024 byte offset. |
| BlockFifoRequest request = {.command = {.opcode = BLOCK_OPCODE_READ}, |
| .vmoid = vmoid, |
| .length = 1, |
| .vmo_offset = 1, |
| .dev_offset = 0}; |
| |
| FakeBackendForBlock::last_data_offset_ = 0xCAFE; |
| |
| EXPECT_OK(client.value()->FifoTransaction(&request, 1)); |
| |
| EXPECT_EQ(FakeBackendForBlock::last_data_offset_, 1024ul); |
| } |
| |
| TEST_F(BlockDriverTest, MapperService) { |
| StartDriver(); |
| |
| zx::result connect_result = |
| driver_test().Connect<fuchsia_hardware_block_volume::Service::Mapper>(); |
| ASSERT_OK(connect_result); |
| |
| fidl::SyncClient<fuchsia_storage_block::Mapper> client(std::move(connect_result.value())); |
| auto [session_client, session_server] = |
| fidl::Endpoints<fuchsia_storage_block::MapperSession>::Create(); |
| |
| zx::vmo mapping_vmo; |
| ASSERT_OK(zx::vmo::create(512 * 1024, 0, &mapping_vmo)); |
| |
| fuchsia_storage_block::MapperOpenSessionRequest request; |
| request.session(std::move(session_server)); |
| request.mapping_vmo(std::move(mapping_vmo)); |
| auto open_result = client->OpenSession(std::move(request)); |
| ASSERT_OK(open_result); |
| } |
| |
| FUCHSIA_DRIVER_EXPORT2(TestBlockDriver); |
| |
| } // anonymous namespace |