| // Copyright 2020 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/storage/blobfs/blobfs-checker.h" |
| |
| #include <lib/sync/completion.h> |
| |
| #include <block-client/cpp/fake-device.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/storage/blobfs/blob.h" |
| #include "src/storage/blobfs/blobfs.h" |
| #include "src/storage/blobfs/common.h" |
| #include "src/storage/blobfs/format.h" |
| #include "src/storage/blobfs/mkfs.h" |
| #include "src/storage/blobfs/test/blob_utils.h" |
| #include "src/storage/blobfs/test/unit/utils.h" |
| |
| namespace blobfs { |
| namespace { |
| |
| using block_client::FakeBlockDevice; |
| |
| constexpr uint32_t kBlockSize = 512; |
| constexpr uint32_t kNumBlocks = 400 * kBlobfsBlockSize / kBlockSize; |
| |
| // Expose access to ReloadSuperblock(). This allows tests to alter the |
| // Superblock on disk and force blobfs to reload it before running a check. |
| class TestBlobfs : public Blobfs { |
| public: |
| zx_status_t Reload() { return ReloadSuperblock(); } |
| }; |
| |
| class BlobfsCheckerTest : public testing::Test { |
| public: |
| void SetUp() override { |
| auto device = std::make_unique<FakeBlockDevice>(kNumBlocks, kBlockSize); |
| ASSERT_TRUE(device); |
| ASSERT_EQ(FormatFilesystem(device.get(), FilesystemOptions{}), ZX_OK); |
| loop_.StartThread(); |
| |
| ASSERT_EQ( |
| Blobfs::Create(loop_.dispatcher(), std::move(device), MountOptions(), zx::resource(), &fs_), |
| ZX_OK); |
| srand(testing::UnitTest::GetInstance()->random_seed()); |
| } |
| |
| // UpdateSuperblock writes the provided superblock to the block device and |
| // forces blobfs to reload immediately. |
| zx_status_t UpdateSuperblock(Superblock& superblock) { |
| size_t superblock_size = kBlobfsBlockSize * SuperblockBlocks(superblock); |
| DeviceBlockWrite(fs_->Device(), &superblock, superblock_size, kSuperblockOffset); |
| return static_cast<TestBlobfs*>(fs_.get())->Reload(); |
| } |
| |
| // Sync waits for blobfs to sync with the underlying block device. |
| zx_status_t Sync() { |
| sync_completion_t completion; |
| fs_->Sync([&completion](zx_status_t status) { sync_completion_signal(&completion); }); |
| return sync_completion_wait(&completion, zx::duration::infinite().get()); |
| } |
| |
| // AddRandomBlob creates and writes a random blob to the file system as a child |
| // of the provided Vnode. Optionally returns the block the blob starts at if block_out is |
| // provided, and the size of the blob if size_out is provided. |
| void AddRandomBlob(fs::Vnode* node, uint64_t* block_out = nullptr, uint64_t* size_out = nullptr) { |
| std::unique_ptr<BlobInfo> info = GenerateRandomBlob("", 1024); |
| memmove(info->path, info->path + 1, strlen(info->path)); // Remove leading slash. |
| |
| fbl::RefPtr<fs::Vnode> file; |
| ASSERT_EQ(node->Create(info->path, 0, &file), ZX_OK); |
| |
| size_t actual; |
| EXPECT_EQ(file->Truncate(info->size_data), ZX_OK); |
| EXPECT_EQ(file->Write(info->data.get(), info->size_data, 0, &actual), ZX_OK); |
| EXPECT_EQ(actual, info->size_data); |
| EXPECT_EQ(file->Close(), ZX_OK); |
| |
| if (block_out) { |
| auto blob = fbl::RefPtr<Blob>::Downcast(file); |
| // Get the block that contains the blob. |
| *block_out = fs_->GetNode(blob->Ino())->extents[0].Start() + DataStartBlock(fs_->Info()); |
| } |
| if (size_out) { |
| *size_out = info->size_data; |
| } |
| } |
| |
| // Creates and writes a corrupt blob to the file system as a child of the provided Vnode. |
| void AddCorruptBlob(fs::Vnode* node) { |
| uint64_t block, size; |
| AddRandomBlob(node, &block, &size); |
| |
| // Unmount. |
| std::unique_ptr<block_client::BlockDevice> device = Blobfs::Destroy(std::move(fs_)); |
| |
| // Read the block that contains the blob. |
| storage::VmoBuffer buffer; |
| ASSERT_EQ(buffer.Initialize(device.get(), 1, kBlobfsBlockSize, "test_buffer"), ZX_OK); |
| block_fifo_request_t request = { |
| .opcode = BLOCKIO_READ, |
| .vmoid = buffer.vmoid(), |
| .length = kBlobfsBlockSize / kBlockSize, |
| .vmo_offset = 0, |
| .dev_offset = block * kBlobfsBlockSize / kBlockSize, |
| }; |
| ASSERT_EQ(device->FifoTransaction(&request, 1), ZX_OK); |
| |
| // Flip a random bit of the data. |
| auto blob_data = static_cast<uint8_t*>(buffer.Data(0)); |
| size_t rand_index = rand() % size; |
| uint8_t old_val = blob_data[rand_index]; |
| while ((blob_data[rand_index] = static_cast<uint8_t>(rand())) == old_val) { |
| } |
| |
| // Write the block back. |
| request.opcode = BLOCKIO_WRITE; |
| ASSERT_EQ(device->FifoTransaction(&request, 1), ZX_OK); |
| |
| // Remount. |
| ASSERT_EQ( |
| Blobfs::Create(loop_.dispatcher(), std::move(device), MountOptions(), zx::resource(), &fs_), |
| ZX_OK); |
| } |
| |
| std::unique_ptr<Blobfs> get_fs_unique() { return std::move(fs_); } |
| Blobfs* get_fs() { return fs_.get(); } |
| |
| protected: |
| bool enable_paging = false; |
| |
| private: |
| async::Loop loop_{&kAsyncLoopConfigNoAttachToCurrentThread}; |
| std::unique_ptr<Blobfs> fs_; |
| }; |
| |
| class BlobfsCheckerPagedTest : public BlobfsCheckerTest { |
| public: |
| void SetUp() override { |
| enable_paging = true; |
| BlobfsCheckerTest::SetUp(); |
| } |
| }; |
| |
| void RunTestEmpty(BlobfsCheckerTest* t) { |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_OK); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestEmpty) { RunTestEmpty(this); } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestEmpty) { RunTestEmpty(this); } |
| |
| void RunTestNonEmpty(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| fs::Vnode* root_node = root.get(); |
| for (unsigned i = 0; i < 3; i++) { |
| t->AddRandomBlob(root_node); |
| } |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_OK); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestNonEmpty) { RunTestNonEmpty(this); } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestNonEmpty) { RunTestNonEmpty(this); } |
| |
| void RunTestInodeWithUnallocatedBlock(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| fs::Vnode* root_node = root.get(); |
| for (unsigned i = 0; i < 3; i++) { |
| t->AddRandomBlob(root_node); |
| } |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| Extent e(1, 1); |
| t->get_fs()->GetAllocator()->FreeBlocks(e); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestInodeWithUnallocatedBlock) { RunTestInodeWithUnallocatedBlock(this); } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestInodeWithUnallocatedBlock) { |
| RunTestInodeWithUnallocatedBlock(this); |
| } |
| |
| // TODO(https://bugs.fuchsia.dev/45924): determine why running this test on an |
| // empty blobfs fails on ASAN QEMU bot. |
| void RunTestAllocatedBlockCountTooHigh(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| t->AddRandomBlob(root.get()); |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| Superblock superblock = t->get_fs()->Info(); |
| superblock.alloc_block_count++; |
| ASSERT_EQ(t->UpdateSuperblock(superblock), ZX_OK); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestAllocatedBlockCountTooHigh) { |
| RunTestAllocatedBlockCountTooHigh(this); |
| } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestAllocatedBlockCountTooHigh) { |
| RunTestAllocatedBlockCountTooHigh(this); |
| } |
| |
| void RunTestAllocatedBlockCountTooLow(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| fs::Vnode* root_node = root.get(); |
| for (unsigned i = 0; i < 3; i++) { |
| t->AddRandomBlob(root_node); |
| } |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| Superblock superblock = t->get_fs()->Info(); |
| superblock.alloc_block_count = 2; |
| t->UpdateSuperblock(superblock); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestAllocatedBlockCountTooLow) { RunTestAllocatedBlockCountTooLow(this); } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestAllocatedBlockCountTooLow) { |
| RunTestAllocatedBlockCountTooLow(this); |
| } |
| |
| void RunTestFewerThanMinimumBlocksAllocated(BlobfsCheckerTest* t) { |
| Extent e(0, 1); |
| t->get_fs()->GetAllocator()->FreeBlocks(e); |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestFewerThanMinimumBlocksAllocated) { |
| RunTestFewerThanMinimumBlocksAllocated(this); |
| } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestFewerThanMinimumBlocksAllocated) { |
| RunTestFewerThanMinimumBlocksAllocated(this); |
| } |
| |
| void RunTestAllocatedInodeCountTooHigh(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| t->AddRandomBlob(root.get()); |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| Superblock superblock = t->get_fs()->Info(); |
| superblock.alloc_inode_count++; |
| t->UpdateSuperblock(superblock); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestAllocatedInodeCountTooHigh) { |
| RunTestAllocatedInodeCountTooHigh(this); |
| } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestAllocatedInodeCountTooHigh) { |
| RunTestAllocatedInodeCountTooHigh(this); |
| } |
| |
| void RunTestAllocatedInodeCountTooLow(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| fs::Vnode* root_node = root.get(); |
| for (unsigned i = 0; i < 3; i++) { |
| t->AddRandomBlob(root_node); |
| } |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| Superblock superblock = t->get_fs()->Info(); |
| superblock.alloc_inode_count = 2; |
| t->UpdateSuperblock(superblock); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestAllocatedInodeCountTooLow) { RunTestAllocatedInodeCountTooLow(this); } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestAllocatedInodeCountTooLow) { |
| RunTestAllocatedInodeCountTooLow(this); |
| } |
| |
| void RunTestCorruptBlobs(BlobfsCheckerTest* t) { |
| fbl::RefPtr<fs::Vnode> root; |
| for (unsigned i = 0; i < 5; i++) { |
| // Need to get the root node inside the loop because adding a corrupt blob causes us to change |
| // the Blobfs instance. The only feasible way right now to corrupt a blob *after* it has been |
| // written out involves unmounting and then remounting the file system. |
| ASSERT_EQ(t->get_fs()->OpenRootNode(&root), ZX_OK); |
| fs::Vnode* root_node = root.get(); |
| if (i % 2 == 0) { |
| t->AddRandomBlob(root_node); |
| } else { |
| t->AddCorruptBlob(root_node); |
| } |
| } |
| EXPECT_EQ(t->Sync(), ZX_OK); |
| |
| BlobfsChecker checker(t->get_fs_unique()); |
| ASSERT_EQ(checker.Check(), ZX_ERR_BAD_STATE); |
| } |
| |
| TEST_F(BlobfsCheckerTest, TestCorruptBlobs) { RunTestCorruptBlobs(this); } |
| |
| TEST_F(BlobfsCheckerPagedTest, TestCorruptBlobs) { RunTestCorruptBlobs(this); } |
| |
| } // namespace |
| } // namespace blobfs |