| // 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 "skip-block.h" |
| |
| #include <lib/fake_ddk/fake_ddk.h> |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/zx/vmo.h> |
| #include <zircon/types.h> |
| |
| #include <optional> |
| |
| #include <ddktl/protocol/badblock.h> |
| #include <ddktl/protocol/nand.h> |
| #include <fbl/vector.h> |
| #include <zxtest/zxtest.h> |
| |
| constexpr uint32_t kPageSize = 1024; |
| constexpr uint32_t kOobSize = 8; |
| constexpr uint32_t kNumPages = 20; |
| constexpr uint32_t kBlockSize = kPageSize * kNumPages; |
| constexpr uint32_t kNumBlocks = 10; |
| constexpr uint32_t kEccBits = 10; |
| |
| fuchsia_hardware_nand_Info kInfo = {kPageSize, kNumPages, kNumBlocks, kEccBits, kOobSize, 0, {}}; |
| |
| // We inject a special context as parent so that we can store the device. |
| struct Context { |
| nand::SkipBlockDevice* dev = nullptr; |
| }; |
| |
| class Binder : public fake_ddk::Bind { |
| public: |
| zx_status_t DeviceRemove(zx_device_t* dev) override { |
| Context* context = reinterpret_cast<Context*>(dev); |
| if (context->dev) { |
| context->dev->DdkRelease(); |
| } |
| context->dev = nullptr; |
| return ZX_OK; |
| } |
| zx_status_t DeviceAdd(zx_driver_t* drv, zx_device_t* parent, device_add_args_t* args, |
| zx_device_t** out) override { |
| *out = parent; |
| Context* context = reinterpret_cast<Context*>(parent); |
| context->dev = reinterpret_cast<nand::SkipBlockDevice*>(args->ctx); |
| |
| if (args && args->ops) { |
| if (args->ops->message) { |
| zx_status_t status; |
| if ((status = fidl_.SetMessageOp(args->ctx, args->ops->message)) < 0) { |
| return status; |
| } |
| } |
| } |
| return ZX_OK; |
| } |
| zx_status_t DeviceGetProtocol(const zx_device_t* device, uint32_t proto_id, |
| void* protocol) override { |
| auto out = reinterpret_cast<fake_ddk::Protocol*>(protocol); |
| for (const auto& proto : protocols_) { |
| if (proto_id == proto.id) { |
| out->ops = proto.proto.ops; |
| out->ctx = proto.proto.ctx; |
| return ZX_OK; |
| } |
| } |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| }; |
| |
| // Fake for the nand protocol. |
| class FakeNand : public ddk::NandProtocol<FakeNand> { |
| public: |
| FakeNand() : proto_({&nand_protocol_ops_, this}) { |
| ASSERT_OK(mapper_.CreateAndMap(kNumBlocks * kNumPages * kPageSize, |
| ZX_VM_PERM_READ | ZX_VM_PERM_WRITE)); |
| } |
| |
| const nand_protocol_t* proto() const { return &proto_; } |
| |
| void set_result(zx_status_t result) { result_.push_back(result); } |
| |
| // Nand protocol: |
| void NandQuery(fuchsia_hardware_nand_Info* info_out, size_t* nand_op_size_out) { |
| *info_out = nand_info_; |
| *nand_op_size_out = sizeof(nand_operation_t); |
| } |
| |
| void NandQueue(nand_operation_t* op, nand_queue_callback completion_cb, void* cookie) { |
| last_op_ = op->command; |
| |
| auto result = result_[call_++]; |
| |
| if (result != ZX_OK) { |
| completion_cb(cookie, result, op); |
| return; |
| } |
| |
| switch (op->command) { |
| case NAND_OP_READ: { |
| if (op->rw.offset_nand >= num_nand_pages_ || !op->rw.length || |
| (num_nand_pages_ - op->rw.offset_nand) < op->rw.length) { |
| result = ZX_ERR_OUT_OF_RANGE; |
| break; |
| } |
| if (op->rw.data_vmo == ZX_HANDLE_INVALID && op->rw.oob_vmo == ZX_HANDLE_INVALID) { |
| result = ZX_ERR_BAD_HANDLE; |
| break; |
| } |
| zx::unowned_vmo data_vmo(op->rw.data_vmo); |
| if (*data_vmo) { |
| auto* data = static_cast<uint8_t*>(mapper_.start()) + (op->rw.offset_nand * kPageSize); |
| data_vmo->read(data, op->rw.offset_data_vmo, op->rw.length * kPageSize); |
| } |
| break; |
| } |
| case NAND_OP_WRITE: { |
| if (op->rw.offset_nand >= num_nand_pages_ || !op->rw.length || |
| (num_nand_pages_ - op->rw.offset_nand) < op->rw.length) { |
| result = ZX_ERR_OUT_OF_RANGE; |
| break; |
| } |
| if (op->rw.data_vmo == ZX_HANDLE_INVALID && op->rw.oob_vmo == ZX_HANDLE_INVALID) { |
| result = ZX_ERR_BAD_HANDLE; |
| break; |
| } |
| zx::unowned_vmo data_vmo(op->rw.data_vmo); |
| if (*data_vmo) { |
| fzl::VmoMapper mapper; |
| result = mapper.Map(*data_vmo, 0, 0, ZX_VM_PERM_READ); |
| if (result != ZX_OK) { |
| break; |
| } |
| auto* src = static_cast<uint8_t*>(mapper.start()) + (op->rw.offset_data_vmo * kPageSize); |
| auto* dst = static_cast<uint8_t*>(mapper_.start()) + (op->rw.offset_nand * kPageSize); |
| memcpy(dst, src, op->rw.length * kPageSize); |
| } |
| break; |
| } |
| case NAND_OP_ERASE: { |
| if (!op->erase.num_blocks || op->erase.first_block >= nand_info_.num_blocks || |
| (op->erase.num_blocks > (nand_info_.num_blocks - op->erase.first_block))) { |
| result = ZX_ERR_OUT_OF_RANGE; |
| break; |
| } |
| auto* data = static_cast<uint8_t*>(mapper_.start()) + |
| (op->erase.first_block * kPageSize * kNumPages); |
| memset(data, 0, op->erase.num_blocks * kPageSize * kNumPages); |
| break; |
| } |
| default: |
| result = ZX_ERR_NOT_SUPPORTED; |
| break; |
| } |
| |
| completion_cb(cookie, result, op); |
| return; |
| } |
| |
| zx_status_t NandGetFactoryBadBlockList(uint32_t* bad_blocks, size_t bad_block_len, |
| size_t* num_bad_blocks) { |
| *num_bad_blocks = 0; |
| return ZX_OK; |
| } |
| |
| void set_block_count(uint32_t num_blocks) { |
| nand_info_.num_blocks = num_blocks; |
| num_nand_pages_ = kNumPages * num_blocks; |
| } |
| |
| const fzl::VmoMapper& mapper() { return mapper_; } |
| const nand_op_t& last_op() { return last_op_; } |
| |
| private: |
| size_t call_ = 0; |
| nand_protocol_t proto_; |
| fuchsia_hardware_nand_Info nand_info_ = kInfo; |
| fbl::Vector<zx_status_t> result_; |
| size_t num_nand_pages_ = kNumPages * kNumBlocks; |
| |
| fzl::VmoMapper mapper_; |
| |
| nand_op_t last_op_ = {}; |
| }; |
| |
| // Fake for the bad block protocol. |
| class FakeBadBlock : public ddk::BadBlockProtocol<FakeBadBlock> { |
| public: |
| FakeBadBlock() : proto_({&bad_block_protocol_ops_, this}) {} |
| |
| const bad_block_protocol_t* proto() const { return &proto_; } |
| |
| void set_result(zx_status_t result) { result_ = result; } |
| |
| // Bad Block protocol: |
| zx_status_t BadBlockGetBadBlockList(uint32_t* bad_block_list, size_t bad_block_list_len, |
| size_t* bad_block_count) { |
| *bad_block_count = grown_bad_blocks_.size(); |
| if (bad_block_list_len < *bad_block_count) { |
| return bad_block_list == nullptr ? ZX_OK : ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| memcpy(bad_block_list, grown_bad_blocks_.data(), grown_bad_blocks_.size() * sizeof(uint32_t)); |
| return result_; |
| } |
| |
| zx_status_t BadBlockMarkBlockBad(uint32_t block) { |
| if (result_ == ZX_OK) { |
| grown_bad_blocks_.push_back(block); |
| } |
| return result_; |
| } |
| |
| const fbl::Vector<uint32_t>& grown_bad_blocks() { return grown_bad_blocks_; } |
| |
| private: |
| fbl::Vector<uint32_t> grown_bad_blocks_; |
| bad_block_protocol_t proto_; |
| zx_status_t result_ = ZX_OK; |
| }; |
| |
| class SkipBlockTest : public zxtest::Test { |
| protected: |
| SkipBlockTest() { |
| fbl::Array<fake_ddk::ProtocolEntry> protocols(new fake_ddk::ProtocolEntry[2], 2); |
| protocols[0] = {ZX_PROTOCOL_NAND, *reinterpret_cast<const fake_ddk::Protocol*>(nand_.proto())}; |
| protocols[1] = {ZX_PROTOCOL_BAD_BLOCK, |
| *reinterpret_cast<const fake_ddk::Protocol*>(bad_block_.proto())}; |
| ddk_.SetProtocols(std::move(protocols)); |
| ddk_.SetSize(kPageSize * kNumPages * kNumBlocks); |
| ddk_.SetMetadata(&count_, sizeof(count_)); |
| } |
| |
| ~SkipBlockTest() { ddk_.DeviceRemove(parent()); } |
| |
| void CreatePayload(size_t size, zx::vmo* out) { |
| zx::vmo vmo; |
| fzl::VmoMapper mapper; |
| ASSERT_OK(mapper.CreateAndMap(fbl::round_up(size, ZX_PAGE_SIZE), |
| ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, &vmo)); |
| memset(mapper.start(), 0x4a, mapper.size()); |
| *out = std::move(vmo); |
| } |
| |
| void Write(nand::ReadWriteOperation op, bool* bad_block_grown, zx_status_t expected = ZX_OK) { |
| if (!client_) { |
| client_.emplace(std::move(ddk().FidlClient())); |
| } |
| |
| auto result = client_->Write(std::move(op)); |
| ASSERT_OK(result.status()); |
| ASSERT_STATUS(result.value().status, expected); |
| *bad_block_grown = result.value().bad_block_grown; |
| } |
| |
| void Read(nand::ReadWriteOperation op, zx_status_t expected = ZX_OK) { |
| if (!client_) { |
| client_.emplace(std::move(ddk().FidlClient())); |
| } |
| auto result = client_->Read(std::move(op)); |
| ASSERT_OK(result.status()); |
| ASSERT_STATUS(result.value().status, expected); |
| } |
| |
| void WriteBytes(nand::WriteBytesOperation op, bool* bad_block_grown, |
| zx_status_t expected = ZX_OK) { |
| if (!client_) { |
| client_.emplace(std::move(ddk().FidlClient())); |
| } |
| auto result = client_->WriteBytes(std::move(op)); |
| ASSERT_OK(result.status()); |
| ASSERT_EQ(result.value().status, expected); |
| *bad_block_grown = result.value().bad_block_grown; |
| } |
| |
| void WriteBytesWithoutErase(nand::WriteBytesOperation op, zx_status_t expected = ZX_OK) { |
| if (!client_) { |
| client_.emplace(std::move(ddk().FidlClient())); |
| } |
| auto result = client_->WriteBytesWithoutErase(std::move(op)); |
| ASSERT_OK(result.status()); |
| ASSERT_STATUS(result.value().status, expected); |
| } |
| |
| void GetPartitionInfo(nand::PartitionInfo* out, zx_status_t expected = ZX_OK) { |
| if (!client_) { |
| client_.emplace(std::move(ddk().FidlClient())); |
| } |
| |
| auto result = client_->GetPartitionInfo(); |
| ASSERT_OK(result.status()); |
| ASSERT_STATUS(result.value().status, expected); |
| *out = result.value().partition_info; |
| } |
| |
| void ValidateWritten(size_t offset, size_t size) { |
| const uint8_t* start = static_cast<uint8_t*>(nand_.mapper().start()) + offset; |
| for (size_t i = 0; i < size; i++) { |
| ASSERT_EQ(start[i], 0x4a, "i = %zu", i); |
| } |
| } |
| |
| void ValidateUnwritten(size_t offset, size_t size) { |
| const uint8_t* start = static_cast<uint8_t*>(nand_.mapper().start()) + offset; |
| for (size_t i = 0; i < size; i++) { |
| ASSERT_EQ(start[i], 0x00, "i = %zu", i); |
| } |
| } |
| |
| zx_device_t* parent() { return reinterpret_cast<zx_device_t*>(&ctx_); } |
| nand::SkipBlockDevice& dev() { return *ctx_.dev; } |
| Binder& ddk() { return ddk_; } |
| FakeNand& nand() { return nand_; } |
| FakeBadBlock& bad_block() { return bad_block_; } |
| |
| private: |
| const uint32_t count_ = 1; |
| Context ctx_ = {}; |
| Binder ddk_; |
| FakeNand nand_; |
| FakeBadBlock bad_block_; |
| std::optional<::llcpp::fuchsia::hardware::skipblock::SkipBlock::SyncClient> client_; |
| }; |
| |
| TEST_F(SkipBlockTest, Create) { ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); } |
| |
| TEST_F(SkipBlockTest, GrowBadBlock) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_ERR_IO); |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 5; |
| op.block_count = 1; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 1); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 5); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| } |
| |
| TEST_F(SkipBlockTest, GrowMultipleBadBlock) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase Block 5 |
| nand().set_result(ZX_OK); |
| // Write Block 5 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 6 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 7 |
| nand().set_result(ZX_OK); |
| // Write Block 7 |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 5; |
| op.block_count = 1; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 2); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 5); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[1], 6); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| } |
| |
| TEST_F(SkipBlockTest, MappingFailure) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase Block 5 |
| nand().set_result(ZX_OK); |
| // Write Block 5 |
| nand().set_result(ZX_ERR_INVALID_ARGS); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 5; |
| op.block_count = 1; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown, ZX_ERR_IO)); |
| ASSERT_FALSE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| } |
| |
| TEST_F(SkipBlockTest, WriteBytesEraseWriteMode) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| // 20 pages in 1 block. |
| // Write page range [110, 130], the corresponding block range is [5, 6] |
| // Erase Block 5 |
| nand().set_result(ZX_OK); |
| // Write Block 5 |
| nand().set_result(ZX_OK); |
| // Erase Block 6 |
| nand().set_result(ZX_OK); |
| // Write Block 6 |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(20 * kPageSize, &vmo)); |
| fzl::VmoMapper mapper; |
| ASSERT_OK(mapper.Map(vmo, 0, 0, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE)); |
| auto start = static_cast<uint8_t*>(nand().mapper().start()) + 5 * kBlockSize; |
| memset(start, 0xab, 2 * kBlockSize); |
| |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.offset = 110 * kPageSize; |
| op.size = 20 * kPageSize; |
| op.mode = nand::WriteBytesMode::ERASE_WRITE; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(WriteBytes(std::move(op), &bad_block_grown)); |
| ASSERT_FALSE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(110 * kPageSize, 20 * kPageSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(100 * kPageSize, 10 * kPageSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(130 * kPageSize, 10 * kPageSize)); |
| } |
| |
| TEST_F(SkipBlockTest, ReadSuccess) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read Block 5. |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 5; |
| op.block_count = 1; |
| |
| ASSERT_NO_FATAL_FAILURES(Read(std::move(op), ZX_OK)); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_READ); |
| } |
| |
| TEST_F(SkipBlockTest, ReadFailure) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read Block 7. |
| nand().set_result(ZX_ERR_INVALID_ARGS); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 7; |
| op.block_count = 1; |
| |
| ASSERT_NO_FATAL_FAILURES(Read(std::move(op), ZX_ERR_IO)); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_READ); |
| } |
| |
| TEST_F(SkipBlockTest, ReadMultipleCopies) { |
| const uint32_t count_ = 4; |
| ddk().SetMetadata(&count_, sizeof(count_)); |
| ddk().SetSize(kPageSize * kNumPages * 8); |
| nand().set_block_count(8); |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read Block 1 |
| nand().set_result(ZX_ERR_IO); |
| // Read Block 3 |
| nand().set_result(ZX_ERR_IO); |
| // Read Block 5 |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 1; |
| op.block_count = 1; |
| |
| ASSERT_NO_FATAL_FAILURES(Read(std::move(op))); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_READ); |
| } |
| |
| TEST_F(SkipBlockTest, ReadMultipleCopiesNoneSucceeds) { |
| const uint32_t count_ = 4; |
| ddk().SetMetadata(&count_, sizeof(count_)); |
| ddk().SetSize(kPageSize * kNumPages * 4); |
| nand().set_block_count(4); |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read Block 0 |
| nand().set_result(ZX_ERR_IO); |
| // Read Block 1 |
| nand().set_result(ZX_ERR_IO); |
| // Read Block 2 |
| nand().set_result(ZX_ERR_IO); |
| // Read Block 3 |
| nand().set_result(ZX_ERR_IO); |
| |
| zx::vmo vmo; |
| ASSERT_OK(zx::vmo::create(fbl::round_up(kBlockSize, ZX_PAGE_SIZE), 0, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 0; |
| op.block_count = 1; |
| |
| ASSERT_NO_FATAL_FAILURES(Read(std::move(op), ZX_ERR_IO)); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_READ); |
| } |
| |
| TEST_F(SkipBlockTest, WriteBytesSingleBlockNoOffset) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read block 5. |
| nand().set_result(ZX_OK); |
| // Erase block 5. |
| nand().set_result(ZX_OK); |
| // Write block 5. |
| nand().set_result(ZX_OK); |
| |
| constexpr size_t kSize = kBlockSize - kPageSize; |
| constexpr size_t kNandOffset = 5 * kBlockSize; |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kSize, &vmo)); |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.offset = kNandOffset; |
| op.size = kSize; |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(WriteBytes(std::move(op), &bad_block_grown)); |
| ASSERT_FALSE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kNandOffset, kSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kNandOffset + kSize, kPageSize)); |
| } |
| |
| TEST_F(SkipBlockTest, WriteBytesSingleBlockWithOffset) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read block 5. |
| nand().set_result(ZX_OK); |
| // Erase block 5. |
| nand().set_result(ZX_OK); |
| // Write block 5. |
| nand().set_result(ZX_OK); |
| |
| constexpr size_t kOffset = kPageSize; |
| constexpr size_t kSize = kBlockSize - (2 * kPageSize); |
| constexpr size_t kNandOffset = 5 * kBlockSize; |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kSize, &vmo)); |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.offset = kNandOffset + kOffset; |
| op.size = kSize; |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(WriteBytes(std::move(op), &bad_block_grown)); |
| ASSERT_FALSE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kNandOffset, kPageSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kNandOffset + kOffset, kSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kNandOffset + kOffset + kSize, kPageSize)); |
| } |
| |
| TEST_F(SkipBlockTest, WriteBytesMultipleBlocks) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Read block 4. |
| nand().set_result(ZX_OK); |
| // Read block 6. |
| nand().set_result(ZX_OK); |
| // Erase block 4 - 6. |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| // Write block 4 - 6. |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| |
| constexpr size_t kOffset = kPageSize; |
| constexpr size_t kSize = (kBlockSize * 3) - (2 * kPageSize); |
| constexpr size_t kNandOffset = 4 * kBlockSize; |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kSize, &vmo)); |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.offset = kNandOffset + kOffset; |
| op.size = kSize; |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(WriteBytes(std::move(op), &bad_block_grown)); |
| ASSERT_FALSE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kNandOffset, kPageSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kNandOffset + kOffset, kSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kNandOffset + kOffset + kSize, kPageSize)); |
| } |
| |
| TEST_F(SkipBlockTest, WriteBytesAligned) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase block 4 - 5. |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| // Write block 4 - 5. |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| nand().set_result(ZX_OK); |
| |
| constexpr size_t kSize = (kBlockSize * 2); |
| constexpr size_t kNandOffset = 4 * kBlockSize; |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kSize, &vmo)); |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.offset = kNandOffset; |
| op.size = kSize; |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(WriteBytes(std::move(op), &bad_block_grown)); |
| ASSERT_FALSE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kNandOffset, kSize)); |
| } |
| |
| TEST_F(SkipBlockTest, WriteBytesWithoutErase) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Write block 5 directly without erase |
| nand().set_result(ZX_OK); |
| |
| // Write the second page of block 5 |
| constexpr size_t kSize = kPageSize; |
| constexpr size_t kNandOffset = 5 * kBlockSize + kPageSize; |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kSize, &vmo)); |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.offset = kNandOffset; |
| op.size = kSize; |
| // The option doesn't have effect, but we still need to give a valid value. |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| |
| ASSERT_NO_FATAL_FAILURES(WriteBytesWithoutErase(std::move(op))); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 0); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kNandOffset, kSize)); |
| } |
| |
| TEST_F(SkipBlockTest, GrownMultipleBadBlocksWriteBytesWithoutEraseFollowedByWriteBytes) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| // Bad block 5 and 6 |
| |
| // Writing starting from page 1 in block 5 |
| constexpr size_t kBlockOffset = 5; |
| constexpr size_t kSize = kPageSize; |
| constexpr size_t kNandOffset = kBlockOffset * kBlockSize + kPageSize; |
| |
| // Backed up read. |
| nand().set_result(ZX_OK); |
| // Write block 5 directly without erase and fails |
| nand().set_result(ZX_ERR_IO); |
| // Fall back writes. |
| // Erase Block 5 |
| nand().set_result(ZX_OK); |
| // Write Block 5. But find that it becomes bad. |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 6. Find it bad as well. |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 7 |
| nand().set_result(ZX_OK); |
| // Write Block 7 |
| nand().set_result(ZX_OK); |
| |
| // Test the safe way of using WriteBytesWithoutErase. |
| |
| // Backed up the minimal block range that covers the write range. |
| // In this case it is block 5. |
| zx::vmo vmo_backed_up; |
| ASSERT_OK(zx::vmo::create(kBlockSize, 0, &vmo_backed_up)); |
| { |
| zx::vmo dup; |
| vmo_backed_up.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(dup); |
| op.block = 5; |
| op.block_count = 1; |
| ASSERT_NO_FATAL_FAILURES(Read(std::move(op), ZX_OK)); |
| } |
| |
| zx::vmo data; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kSize, &data)); |
| fzl::VmoMapper mapper; |
| ASSERT_OK(mapper.Map(data, 0, 0, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE)); |
| // Update the backed up data with the new data. |
| vmo_backed_up.write(mapper.start(), kPageSize, kSize); |
| |
| // Attempt to write without erase. |
| { |
| nand::WriteBytesOperation op; |
| op.vmo = std::move(data); |
| op.offset = kNandOffset; |
| op.size = kSize; |
| // The option doesn't have effect, but we still need to give a valid value. |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| ASSERT_NO_FATAL_FAILURES(WriteBytesWithoutErase(std::move(op), ZX_ERR_IO)); |
| } |
| |
| // Fall back writes on the minimal block range. |
| { |
| nand::WriteBytesOperation op = {}; |
| op.vmo = std::move(vmo_backed_up); |
| op.offset = kBlockOffset * kBlockSize; |
| op.size = kBlockSize; |
| // The option doesn't have effect, but we still need to give a valid value. |
| op.mode = nand::WriteBytesMode::READ_MODIFY_ERASE_WRITE; |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(WriteBytes(std::move(op), &bad_block_grown)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 2); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 5); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[1], 6); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| } |
| |
| // Validate content, expected to be at block 7. |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(7 * kBlockSize + kPageSize, kSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(7 * kBlockSize, kPageSize)); |
| ASSERT_NO_FATAL_FAILURES( |
| ValidateUnwritten(7 * kBlockSize + 2 * kPageSize, kBlockSize - 2 * kPageSize)); |
| } |
| |
| TEST_F(SkipBlockTest, GetPartitionInfo) { |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| nand::PartitionInfo info; |
| ASSERT_NO_FATAL_FAILURES(GetPartitionInfo(&info)); |
| ASSERT_EQ(info.block_size_bytes, kBlockSize); |
| ASSERT_EQ(info.partition_block_count, kNumBlocks); |
| } |
| |
| // This test attempts to write 2 copies of a single block to a partition that is 10 blocks wide. |
| // The copies of logical block 1, start out as block 1 and 6. After erase or write failures, the |
| // blocks are marked bad, and blocks 2 and 7 become the new "physical" copies of logical block 1. |
| TEST_F(SkipBlockTest, WriteMultipleCopies) { |
| const uint32_t count_ = 2; |
| ddk().SetMetadata(&count_, sizeof(count_)); |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase Block 1 |
| nand().set_result(ZX_OK); |
| // Write Block 1 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 2 |
| nand().set_result(ZX_OK); |
| // Write Block 2 |
| nand().set_result(ZX_OK); |
| // Erase Block 6 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 7 |
| nand().set_result(ZX_OK); |
| // Write Block 7 |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kBlockSize, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 1; |
| op.block_count = 1; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 2); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 1); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[1], 6); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kBlockSize * 1, kBlockSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kBlockSize * 2, kBlockSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kBlockSize * 6, kBlockSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kBlockSize * 7, kBlockSize)); |
| } |
| |
| // This test attempts to write 2 copies of two blocks to a partition that is 10 blocks wide. |
| // The copies of logical block 1, start out as block 1 and 6. After erase or write failures, the |
| // blocks are marked bad, and blocks 2 and 7 become the new "physical" copies of logical block 1, |
| // and 3 and 8 becomes the new "physical" copies of logical block 2. |
| TEST_F(SkipBlockTest, WriteMultipleCopiesMultipleBlocks) { |
| const uint32_t count_ = 2; |
| ddk().SetMetadata(&count_, sizeof(count_)); |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase Block 1. |
| nand().set_result(ZX_OK); |
| // Write Block 1. |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 2. |
| nand().set_result(ZX_OK); |
| // Write Block 2. |
| nand().set_result(ZX_OK); |
| // Erase Block 3. |
| nand().set_result(ZX_OK); |
| // Write Block 3. |
| nand().set_result(ZX_OK); |
| // Erase Block 6. |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 7. |
| nand().set_result(ZX_OK); |
| // Write Block 7. |
| nand().set_result(ZX_OK); |
| // Erase Block 8. |
| nand().set_result(ZX_OK); |
| // Write Block 8. |
| nand().set_result(ZX_OK); |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kBlockSize * 2, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 1; |
| op.block_count = 2; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 2); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 1); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[1], 6); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kBlockSize * 1, kBlockSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kBlockSize * 2, kBlockSize * 2)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kBlockSize * 6, kBlockSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kBlockSize * 7, kBlockSize * 2)); |
| } |
| |
| // This test attempts to write 4 copies of a single block to a partition that is 4 blocks wide. |
| // The copies of logical block 0, live in blocks 0, 1, 2, and 3. Since there are no extra copies of |
| // the blocks, a write/erase failure doesn't result in a new physical block for that copy being |
| // written. Instead we just continue to next copy. Despite only one copy of the block being written |
| // successfully, the write request succeeds. We validate all failed blocks are bad blocks grown. |
| TEST_F(SkipBlockTest, WriteMultipleCopiesOneSucceeds) { |
| const uint32_t count_ = 4; |
| ddk().SetMetadata(&count_, sizeof(count_)); |
| ddk().SetSize(kPageSize * kNumPages * 4); |
| nand().set_block_count(4); |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase Block 0 |
| nand().set_result(ZX_OK); |
| // Write Block 0 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 1 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 2 |
| nand().set_result(ZX_OK); |
| // Write Block 2 |
| nand().set_result(ZX_OK); |
| // Erase Block 3 |
| nand().set_result(ZX_OK); |
| // Write Block 3 |
| nand().set_result(ZX_ERR_IO); |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kBlockSize, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 0; |
| op.block_count = 1; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 3); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 0); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[1], 1); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[2], 3); |
| ASSERT_EQ(nand().last_op(), NAND_OP_WRITE); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(0, kBlockSize * 2)); |
| ASSERT_NO_FATAL_FAILURES(ValidateWritten(kBlockSize * 2, kBlockSize)); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(kBlockSize * 3, kBlockSize)); |
| } |
| |
| // This test attempts to write 4 copies of a single block. The copies live in blocks 0, 1, 2, and 3. |
| // The first operation, a block erase, fails for each copy of the block. We validate that the |
| // overall write also fails and all failed blocks are grown bad blocks. |
| TEST_F(SkipBlockTest, WriteMultipleCopiesNoneSucceeds) { |
| const uint32_t count_ = 4; |
| ddk().SetMetadata(&count_, sizeof(count_)); |
| ddk().SetSize(kPageSize * kNumPages * 4); |
| nand().set_block_count(4); |
| ASSERT_OK(nand::SkipBlockDevice::Create(nullptr, parent())); |
| |
| // Erase Block 0 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 1 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 2 |
| nand().set_result(ZX_ERR_IO); |
| // Erase Block 3 |
| nand().set_result(ZX_ERR_IO); |
| |
| zx::vmo vmo; |
| ASSERT_NO_FATAL_FAILURES(CreatePayload(kBlockSize, &vmo)); |
| nand::ReadWriteOperation op = {}; |
| op.vmo = std::move(vmo); |
| op.block = 0; |
| op.block_count = 1; |
| |
| bool bad_block_grown; |
| ASSERT_NO_FATAL_FAILURES(Write(std::move(op), &bad_block_grown, ZX_ERR_IO)); |
| ASSERT_TRUE(bad_block_grown); |
| ASSERT_EQ(bad_block().grown_bad_blocks().size(), 4); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[0], 0); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[1], 1); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[2], 2); |
| ASSERT_EQ(bad_block().grown_bad_blocks()[3], 3); |
| ASSERT_EQ(nand().last_op(), NAND_OP_ERASE); |
| ASSERT_NO_FATAL_FAILURES(ValidateUnwritten(0, kBlockSize * 4)); |
| } |