blob: 061b449e101b8612bdad3e69120cc740953c74ad [file] [log] [blame]
// 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 "src/storage/minfs/fsck.h"
#include <fcntl.h>
#include <lib/sync/completion.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <block-client/cpp/fake-device.h>
#include <fbl/auto_call.h>
#include <fs-management/mount.h>
#include <fs/journal/format.h>
#include <safemath/checked_math.h>
#include <zxtest/zxtest.h>
#include "src/storage/minfs/format.h"
#include "src/storage/minfs/minfs_private.h"
namespace minfs {
namespace {
using block_client::FakeBlockDevice;
constexpr uint64_t kBlockCount = 1 << 20;
constexpr uint32_t kBlockSize = 512;
class ConsistencyCheckerFixture : public zxtest::Test {
public:
void SetUp() override { device_ = std::make_unique<FakeBlockDevice>(kBlockCount, kBlockSize); }
std::unique_ptr<FakeBlockDevice> take_device() { return std::move(device_); }
private:
std::unique_ptr<FakeBlockDevice> device_;
};
using ConsistencyCheckerTest = ConsistencyCheckerFixture;
TEST_F(ConsistencyCheckerTest, NewlyFormattedFilesystemWithRepair) {
auto device = take_device();
std::unique_ptr<Bcache> bcache;
ASSERT_OK(Bcache::Create(std::move(device), kBlockCount, &bcache));
ASSERT_OK(Mkfs(bcache.get()));
ASSERT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}));
}
TEST_F(ConsistencyCheckerTest, NewlyFormattedFilesystemWithoutRepair) {
auto device = take_device();
std::unique_ptr<Bcache> bcache;
ASSERT_OK(Bcache::Create(std::move(device), kBlockCount, &bcache));
ASSERT_OK(Mkfs(bcache.get()));
ASSERT_OK(Fsck(std::move(bcache), FsckOptions()));
}
TEST_F(ConsistencyCheckerTest, NewlyFormattedFilesystemCheckAfterMount) {
auto device = take_device();
std::unique_ptr<Bcache> bcache;
ASSERT_OK(Bcache::Create(std::move(device), kBlockCount, &bcache));
ASSERT_OK(Mkfs(bcache.get()));
MountOptions options = {};
std::unique_ptr<Minfs> fs;
ASSERT_OK(Minfs::Create(std::move(bcache), options, &fs));
bcache = Minfs::Destroy(std::move(fs));
ASSERT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}));
}
class ConsistencyCheckerFixtureVerbose : public zxtest::Test {
public:
void SetUp() override {
auto device = std::make_unique<FakeBlockDevice>(kBlockCount, kMinfsBlockSize);
std::unique_ptr<Bcache> bcache;
EXPECT_OK(Bcache::Create(std::move(device), kBlockCount, &bcache));
EXPECT_OK(Mkfs(bcache.get()));
MountOptions options = {};
EXPECT_OK(Minfs::Create(std::move(bcache), options, &fs_));
}
Minfs* get_fs() const { return fs_.get(); }
void destroy_fs(std::unique_ptr<Bcache>* bcache) {
sync_completion_t completion;
fs_->Sync([&completion](zx_status_t status) { sync_completion_signal(&completion); });
EXPECT_OK(sync_completion_wait(&completion, zx::duration::infinite().get()));
*bcache = Minfs::Destroy(std::move(fs_));
}
fs::VnodeAttributes CreateAndWrite(const char* name, size_t truncate_size, size_t offset,
size_t data_size) {
fbl::RefPtr<VnodeMinfs> root;
EXPECT_OK(fs_->VnodeGet(&root, kMinfsRootIno));
fbl::RefPtr<fs::Vnode> child;
EXPECT_OK(root->Create(name, 0, &child));
if (data_size != 0) {
char data[data_size];
memset(data, 0, data_size);
size_t size_written;
EXPECT_OK(child->Write(data, data_size, offset, &size_written));
EXPECT_EQ(size_written, data_size);
}
if (truncate_size > 0) {
EXPECT_OK(child->Truncate(truncate_size));
}
fs::VnodeAttributes stat;
EXPECT_OK(child->GetAttributes(&stat));
EXPECT_OK(child->Close());
return stat;
}
void TearDown() override { EXPECT_NULL(fs_.get()); }
Minfs& fs() { return *fs_; }
std::unique_ptr<Minfs> TakeFs() { return std::move(fs_); }
void MarkDirectoryEntryMissing(size_t offset, std::unique_ptr<Bcache>* bcache);
private:
std::unique_ptr<Minfs> fs_;
};
TEST_F(ConsistencyCheckerFixtureVerbose, TwoInodesPointToABlock) {
fs::VnodeAttributes file1_stat = {}, file2_stat = {};
{
// Create a file with one data block.
file1_stat = CreateAndWrite("file1", 0, 0, kMinfsBlockSize);
}
{
// Create an empty file.
file2_stat = CreateAndWrite("file2", 0, 0, 0);
}
EXPECT_NE(file1_stat.inode, file2_stat.inode);
// To keep test simple, we ensure here that inodes allocated for file1 and
// file2 are within the same block in the inode table.
EXPECT_EQ(file1_stat.inode / kMinfsInodesPerBlock, file2_stat.inode / kMinfsInodesPerBlock);
std::unique_ptr<Bcache> bcache;
destroy_fs(&bcache);
Superblock sb;
EXPECT_OK(bcache->Readblk(0, &sb));
Inode inodes[kMinfsInodesPerBlock];
blk_t inode_block =
safemath::checked_cast<uint32_t>(sb.ino_block + (file1_stat.inode / kMinfsInodesPerBlock));
EXPECT_OK(bcache->Readblk(inode_block, &inodes));
size_t file1_ino = file1_stat.inode % kMinfsInodesPerBlock;
size_t file2_ino = file2_stat.inode % kMinfsInodesPerBlock;
// The test code has hard dependency on filesystem layout.
// TODO(fxbug.dev/39741): Isolate this test from the on-disk format.
EXPECT_GT(inodes[file1_ino].dnum[0], 0);
EXPECT_EQ(inodes[file2_ino].dnum[0], 0);
// Make second file to point to the block owned by first file.
inodes[file2_ino].dnum[0] = inodes[file1_ino].dnum[0];
inodes[file2_ino].block_count = inodes[file1_ino].block_count;
inodes[file2_ino].size = inodes[file1_ino].size;
EXPECT_OK(bcache->Writeblk(inode_block, inodes));
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}, &bcache));
}
TEST_F(ConsistencyCheckerFixtureVerbose, TwoOffsetsPointToABlock) {
fs::VnodeAttributes file_stat = {};
file_stat = CreateAndWrite("file", 2 * kMinfsBlockSize, 0, kMinfsBlockSize);
std::unique_ptr<Bcache> bcache;
destroy_fs(&bcache);
Superblock sb;
EXPECT_OK(bcache->Readblk(0, &sb));
Inode inodes[kMinfsInodesPerBlock];
blk_t inode_block =
safemath::checked_cast<uint32_t>(sb.ino_block + (file_stat.inode / kMinfsInodesPerBlock));
EXPECT_OK(bcache->Readblk(inode_block, &inodes));
size_t file_ino = file_stat.inode % kMinfsInodesPerBlock;
EXPECT_GT(inodes[file_ino].dnum[0], 0);
EXPECT_EQ(inodes[file_ino].dnum[1], 0);
// Make second block offset point to the first block.
inodes[file_ino].dnum[1] = inodes[file_ino].dnum[0];
EXPECT_OK(bcache->Writeblk(inode_block, inodes));
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}, &bcache));
}
TEST_F(ConsistencyCheckerFixtureVerbose, IndirectBlocksShared) {
fs::VnodeAttributes file_stat = {};
uint64_t double_indirect_offset =
(kMinfsDirect + (kMinfsIndirect * kMinfsDirectPerIndirect) + 1) * kMinfsBlockSize;
file_stat = CreateAndWrite("file", double_indirect_offset, 0, kMinfsBlockSize);
std::unique_ptr<Bcache> bcache;
destroy_fs(&bcache);
Superblock sb;
EXPECT_OK(bcache->Readblk(0, &sb));
Inode inodes[kMinfsInodesPerBlock];
blk_t inode_block =
safemath::checked_cast<uint32_t>(sb.ino_block + (file_stat.inode / kMinfsInodesPerBlock));
EXPECT_OK(bcache->Readblk(inode_block, &inodes));
size_t file_ino = file_stat.inode % kMinfsInodesPerBlock;
EXPECT_GT(inodes[file_ino].dnum[0], 0);
EXPECT_EQ(inodes[file_ino].dnum[1], 0);
EXPECT_EQ(inodes[file_ino].inum[0], 0);
EXPECT_EQ(inodes[file_ino].dinum[0], 0);
// Make various indirect blocks to point to the data block.
inodes[file_ino].dnum[1] = inodes[file_ino].dnum[0];
inodes[file_ino].inum[0] = inodes[file_ino].dnum[0];
inodes[file_ino].dinum[0] = inodes[file_ino].dnum[0];
EXPECT_OK(bcache->Writeblk(inode_block, inodes));
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}, &bcache));
}
void ConsistencyCheckerFixtureVerbose::MarkDirectoryEntryMissing(size_t offset,
std::unique_ptr<Bcache>* bcache) {
blk_t root_dir_block;
{
fbl::RefPtr<VnodeMinfs> root;
EXPECT_OK(fs_->VnodeGet(&root, kMinfsRootIno));
root_dir_block = root->GetInode()->dnum[0] + fs().Info().dat_block;
}
destroy_fs(bcache);
// Need this buffer to be a full block.
DirentBuffer<kMinfsBlockSize> dirent_buffer;
ASSERT_OK((*bcache)->Readblk(root_dir_block, dirent_buffer.raw));
dirent_buffer.dirent.ino = 0;
ASSERT_OK((*bcache)->Writeblk(root_dir_block, dirent_buffer.raw));
}
TEST_F(ConsistencyCheckerFixtureVerbose, MissingDotEntry) {
std::unique_ptr<Bcache> bcache;
MarkDirectoryEntryMissing(0, &bcache);
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}, &bcache));
}
TEST_F(ConsistencyCheckerFixtureVerbose, MissingDotDotEntry) {
std::unique_ptr<Bcache> bcache;
MarkDirectoryEntryMissing(DirentSize(1), &bcache);
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = true}, &bcache));
}
void CreateUnlinkedDirectoryWithEntry(std::unique_ptr<Minfs> fs,
std::unique_ptr<Bcache>* bcache_out) {
ino_t ino;
blk_t inode_block;
{
fbl::RefPtr<VnodeMinfs> root;
ASSERT_OK(fs->VnodeGet(&root, kMinfsRootIno));
fbl::RefPtr<fs::Vnode> child_;
ASSERT_OK(root->Create("foo", 0, &child_));
auto child = fbl::RefPtr<VnodeMinfs>::Downcast(std::move(child_));
auto close_child = fbl::MakeAutoCall([child]() { child->Close(); });
ino = child->GetIno();
ASSERT_GT(kMinfsInodesPerBlock, ino);
// Need this buffer to be a full block.
DirentBuffer<kMinfsBlockSize> dirent_buffer;
uint8_t data[kMinfsBlockSize];
dirent_buffer.dirent.ino = ino;
dirent_buffer.dirent.reclen = DirentSize(1);
dirent_buffer.dirent.namelen = 1;
dirent_buffer.dirent.type = kMinfsTypeDir;
dirent_buffer.dirent.name[0] = '.';
size_t written;
ASSERT_OK(child->Write(data, dirent_buffer.dirent.reclen, 0, &written));
ASSERT_EQ(written, dirent_buffer.dirent.reclen);
ASSERT_OK(root->Unlink("foo", false));
sync_completion_t completion;
fs->Sync([&completion](zx_status_t status) { sync_completion_signal(&completion); });
EXPECT_OK(sync_completion_wait(&completion, zx::duration::infinite().get()));
inode_block = fs->Info().ino_block;
// Prevent the inode from being purged when we close the child.
fs->SetReadonly(true);
fs->StopWriteback();
}
std::unique_ptr<Bcache> bcache = Minfs::Destroy(std::move(fs));
// Now hack the inode so it looks like a directory with an invalid entry count.
Inode inodes[kMinfsInodesPerBlock];
ASSERT_OK(bcache->Readblk(inode_block, &inodes));
Inode& inode = inodes[ino];
inode.magic = kMinfsMagicDir;
inode.dirent_count = 1;
ASSERT_OK(bcache->Writeblk(inode_block, &inodes));
*bcache_out = std::move(bcache);
}
TEST_F(ConsistencyCheckerFixtureVerbose, UnlinkedDirectoryHasBadEntryCount) {
std::unique_ptr<Bcache> bcache;
ASSERT_NO_FATAL_FAILURES(CreateUnlinkedDirectoryWithEntry(TakeFs(), &bcache));
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = false, .read_only = true}, &bcache));
}
TEST_F(ConsistencyCheckerFixtureVerbose, CorruptSuperblock) {
std::unique_ptr<Bcache> bcache;
destroy_fs(&bcache);
Superblock sb;
EXPECT_OK(bcache->Readblk(0, &sb));
// Check if superblock magic is valid
EXPECT_EQ(sb.magic0, kMinfsMagic0);
EXPECT_EQ(sb.magic1, kMinfsMagic1);
// Corrupt the superblock
sb.checksum = 0;
EXPECT_OK(bcache->Writeblk(0, &sb));
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = false}, &bcache));
}
TEST_F(ConsistencyCheckerFixtureVerbose, CorruptJournalInfo) {
std::unique_ptr<Bcache> bcache;
destroy_fs(&bcache);
Superblock sb;
EXPECT_OK(bcache->Readblk(0, &sb));
char data[kMinfsBlockSize];
blk_t journal_block = static_cast<blk_t>(JournalStartBlock(sb));
EXPECT_OK(bcache->Readblk(journal_block, data));
// Check that the journal superblock is valid.
fs::JournalInfo* journal_info = reinterpret_cast<fs::JournalInfo*>(data);
EXPECT_EQ(journal_info->magic, fs::kJournalMagic);
// Corrupt the journalInfo checksum
journal_info->checksum = 0;
EXPECT_OK(bcache->Writeblk(journal_block, data));
ASSERT_NOT_OK(Fsck(std::move(bcache), FsckOptions{.repair = false}, &bcache));
}
} // namespace
} // namespace minfs