blob: e9cf3c4e4831b62041e5aa7618a686ef6be2438e [file] [log] [blame]
// Copyright 2017 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 <errno.h>
#include <fcntl.h>
#include <lib/fzl/fdio.h>
#include <sys/stat.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <thread>
#include <blobfs/common.h>
#include <fvm/format.h>
#include <zxtest/zxtest.h>
#include "blobfs_fixtures.h"
namespace {
using fs::FilesystemTest;
using fs::RamDisk;
// This is work in progress!. See ZX-4203 for context.
/*
// Helper functions for testing:
static bool MakeBlobUnverified(fs_test_utils::BlobInfo* info, fbl::unique_fd* out_fd) {
fbl::unique_fd fd(open(info->path, O_CREAT | O_RDWR));
ASSERT_TRUE(fd, "Failed to create blob");
ASSERT_EQ(ftruncate(fd.get(), info->size_data), 0);
ASSERT_EQ(fs_test_utils::StreamAll(write, fd.get(), info->data.get(), info->size_data), 0,
"Failed to write Data");
out_fd->reset(fd.release());
return true;
}
*/
void RunHugeBlobRandomTest(FilesystemTest* test) {
std::unique_ptr<fs_test_utils::BlobInfo> info;
// This blob is extremely large, and will remain large on disk.
// It is not easily compressible.
size_t kMaxSize = 1 << 25; // 32 MB.
size_t file_size = std::min(kMaxSize, 2 * blobfs::WriteBufferSize() * blobfs::kBlobfsBlockSize);
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath, file_size, &info));
fbl::unique_fd fd;
ASSERT_NO_FAILURES(MakeBlob(info.get(), &fd));
// We can re-open and verify the Blob as read-only.
fd.reset(open(info->path, O_RDONLY));
ASSERT_TRUE(fd, "Failed to-reopen blob");
ASSERT_TRUE(fs_test_utils::VerifyContents(fd.get(), info->data.get(), info->size_data));
// We cannot re-open the blob as writable.
fd.reset(open(info->path, O_RDWR | O_CREAT));
ASSERT_FALSE(fd, "Shouldn't be able to re-create blob that exists");
fd.reset(open(info->path, O_RDWR));
ASSERT_FALSE(fd, "Shouldn't be able to re-open blob as writable");
fd.reset(open(info->path, O_WRONLY));
ASSERT_FALSE(fd, "Shouldn't be able to re-open blob as writable");
// Force decompression by remounting, re-accessing blob.
ASSERT_NO_FAILURES(test->Remount());
fd.reset(open(info->path, O_RDONLY));
ASSERT_TRUE(fd, "Failed to-reopen blob");
ASSERT_TRUE(fs_test_utils::VerifyContents(fd.get(), info->data.get(), info->size_data));
ASSERT_EQ(0, unlink(info->path));
}
TEST_F(BlobfsTest, HugeBlobRandom) { RunHugeBlobRandomTest(this); }
TEST_F(BlobfsTestWithFvm, HugeBlobRandom) { RunHugeBlobRandomTest(this); }
void RunHugeBlobCompressibleTest(FilesystemTest* test) {
std::unique_ptr<fs_test_utils::BlobInfo> info;
// This blob is extremely large, and will remain large on disk, even though
// it is very compressible.
size_t kMaxSize = 1 << 25; // 32 MB.
size_t file_size = std::min(kMaxSize, 2 * blobfs::WriteBufferSize() * blobfs::kBlobfsBlockSize);
ASSERT_TRUE(fs_test_utils::GenerateBlob(
[](char* data, size_t length) {
fs_test_utils::RandomFill(data, length / 2);
data += length / 2;
memset(data, 'a', length / 2);
},
kMountPath, file_size, &info));
fbl::unique_fd fd;
ASSERT_NO_FAILURES(MakeBlob(info.get(), &fd));
// We can re-open and verify the Blob as read-only.
fd.reset(open(info->path, O_RDONLY));
ASSERT_TRUE(fd, "Failed to-reopen blob");
ASSERT_TRUE(fs_test_utils::VerifyContents(fd.get(), info->data.get(), info->size_data));
// We cannot re-open the blob as writable.
fd.reset(open(info->path, O_RDWR | O_CREAT));
ASSERT_FALSE(fd, "Shouldn't be able to re-create blob that exists");
fd.reset(open(info->path, O_RDWR));
ASSERT_FALSE(fd, "Shouldn't be able to re-open blob as writable");
fd.reset(open(info->path, O_WRONLY));
ASSERT_FALSE(fd, "Shouldn't be able to re-open blob as writable");
// Force decompression by remounting, re-accessing blob.
ASSERT_NO_FAILURES(test->Remount());
fd.reset(open(info->path, O_RDONLY));
ASSERT_TRUE(fd, "Failed to-reopen blob");
ASSERT_TRUE(fs_test_utils::VerifyContents(fd.get(), info->data.get(), info->size_data));
}
TEST_F(BlobfsTest, HugeBlobCompressible) { RunHugeBlobCompressibleTest(this); }
TEST_F(BlobfsTestWithFvm, HugeBlobCompressible) { RunHugeBlobCompressibleTest(this); }
/*
static bool CreateUmountRemountLarge(BlobfsTest* blobfsTest) {
BEGIN_HELPER;
fs_test_utils::BlobList bl(MOUNT_PATH);
// TODO(smklein): Here, and elsewhere in this file, remove this source
// of randomness to make the unit test deterministic -- fuzzing should
// be the tool responsible for introducing randomness into the system.
unsigned int seed = static_cast<unsigned int>(zx_ticks_get());
unittest_printf("unmount_remount test using seed: %u\n", seed);
// Do some operations...
size_t num_ops = 5000;
for (size_t i = 0; i < num_ops; ++i) {
switch (rand_r(&seed) % 6) {
case 0:
ASSERT_TRUE(bl.CreateBlob(&seed));
break;
case 1:
ASSERT_TRUE(bl.ConfigBlob());
break;
case 2:
ASSERT_TRUE(bl.WriteData());
break;
case 3:
ASSERT_TRUE(bl.ReadData());
break;
case 4:
ASSERT_TRUE(bl.ReopenBlob());
break;
case 5:
ASSERT_TRUE(bl.UnlinkBlob());
break;
}
}
// Close all currently opened nodes (REGARDLESS of their state)
bl.CloseAll();
// Unmount, remount
ASSERT_TRUE(blobfsTest->Remount(), "Could not re-mount blobfs");
// Reopen all (readable) blobs
bl.OpenAll();
// Verify state of all blobs
bl.VerifyAll();
// Close everything again
bl.CloseAll();
END_HELPER;
}
int unmount_remount_thread(void* arg) {
fs_test_utils::BlobList* bl = static_cast<fs_test_utils::BlobList*>(arg);
unsigned int seed = static_cast<unsigned int>(zx_ticks_get());
unittest_printf("unmount_remount thread using seed: %u\n", seed);
// Do some operations...
size_t num_ops = 1000;
for (size_t i = 0; i < num_ops; ++i) {
switch (rand_r(&seed) % 6) {
case 0:
ASSERT_TRUE(bl->CreateBlob(&seed));
break;
case 1:
ASSERT_TRUE(bl->ConfigBlob());
break;
case 2:
ASSERT_TRUE(bl->WriteData());
break;
case 3:
ASSERT_TRUE(bl->ReadData());
break;
case 4:
ASSERT_TRUE(bl->ReopenBlob());
break;
case 5:
ASSERT_TRUE(bl->UnlinkBlob());
break;
}
}
return 0;
}
static bool CreateUmountRemountLargeMultithreaded(BlobfsTest* blobfsTest) {
BEGIN_HELPER;
fs_test_utils::BlobList bl(MOUNT_PATH);
size_t num_threads = 10;
fbl::AllocChecker ac;
fbl::Array<thrd_t> threads(new (&ac) thrd_t[num_threads](), num_threads);
ASSERT_TRUE(ac.check());
// Launch all threads
for (size_t i = 0; i < num_threads; i++) {
ASSERT_EQ(thrd_create(&threads[i], unmount_remount_thread, &bl),
thrd_success);
}
// Wait for all threads to complete.
// Currently, threads will always return a successful status.
for (size_t i = 0; i < num_threads; i++) {
int res;
ASSERT_EQ(thrd_join(threads[i], &res), thrd_success);
ASSERT_EQ(res, 0);
}
// Close all currently opened nodes (REGARDLESS of their state)
bl.CloseAll();
// Unmount, remount
ASSERT_TRUE(blobfsTest->Remount(), "Could not re-mount blobfs");
// reopen all blobs
bl.OpenAll();
// verify all blob contents
bl.VerifyAll();
// close everything again
bl.CloseAll();
END_HELPER;
}
*/
void RunNoSpaceTest() {
std::unique_ptr<fs_test_utils::BlobInfo> last_info = nullptr;
// Keep generating blobs until we run out of space.
size_t count = 0;
while (true) {
std::unique_ptr<fs_test_utils::BlobInfo> info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath, 1 << 17, &info));
fbl::unique_fd fd(open(info->path, O_CREAT | O_RDWR));
ASSERT_TRUE(fd, "Failed to create blob");
int r = ftruncate(fd.get(), info->size_data);
if (r < 0) {
ASSERT_EQ(errno, ENOSPC, "Blobfs expected to run out of space");
// We ran out of space, as expected. Can we allocate if we
// unlink a previously allocated blob of the desired size?
ASSERT_EQ(unlink(last_info->path), 0, "Unlinking old blob");
ASSERT_EQ(ftruncate(fd.get(), info->size_data), 0, "Re-init after unlink");
// Yay! allocated successfully.
break;
}
ASSERT_EQ(fs_test_utils::StreamAll(write, fd.get(), info->data.get(), info->size_data), 0,
"Failed to write Data");
last_info = std::move(info);
if (++count % 50 == 0) {
printf("Allocated %lu blobs\n", count);
}
}
}
TEST_F(BlobfsTest, NoSpace) { RunNoSpaceTest(); }
TEST_F(BlobfsTestWithFvm, NoSpace) { RunNoSpaceTest(); }
// The following test attempts to fragment the underlying blobfs partition
// assuming a trivial linear allocator. A more intelligent allocator may require
// modifications to this test.
void RunFragmentationTest(FilesystemTest* test) {
// Keep generating blobs until we run out of space, in a pattern of large,
// small, large, small, large.
//
// At the end of the test, we'll free the small blobs, and observe if it is
// possible to allocate a larger blob. With a simple allocator and no
// defragmentation, this would result in a NO_SPACE error.
constexpr size_t kSmallSize = (1 << 16);
constexpr size_t kLargeSize = (1 << 17);
fbl::Vector<fbl::String> small_blobs;
bool do_small_blob = true;
size_t count = 0;
while (true) {
std::unique_ptr<fs_test_utils::BlobInfo> info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath,
do_small_blob ? kSmallSize : kLargeSize, &info));
fbl::unique_fd fd(open(info->path, O_CREAT | O_RDWR));
ASSERT_TRUE(fd, "Failed to create blob");
if (ftruncate(fd.get(), info->size_data) < 0) {
ASSERT_EQ(ENOSPC, errno, "Blobfs expected to run out of space");
break;
}
ASSERT_EQ(0, fs_test_utils::StreamAll(write, fd.get(), info->data.get(), info->size_data),
"Failed to write Data");
if (do_small_blob) {
small_blobs.push_back(fbl::String(info->path));
}
do_small_blob = !do_small_blob;
if (++count % 50 == 0) {
printf("Allocated %lu blobs\n", count);
}
}
// We have filled up the disk with both small and large blobs.
// Observe that we cannot add another large blob.
std::unique_ptr<fs_test_utils::BlobInfo> info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath, kLargeSize, &info));
// Calculate actual number of blocks required to store the blob (including the merkle tree).
blobfs::Inode large_inode;
large_inode.blob_size = kLargeSize;
size_t kLargeBlocks = blobfs::MerkleTreeBlocks(large_inode) + blobfs::BlobDataBlocks(large_inode);
// We shouldn't have space (before we try allocating) ...
fuchsia_io_FilesystemInfo usage;
ASSERT_NO_FAILURES(test->GetFsInfo(&usage));
ASSERT_LT(usage.total_bytes - usage.used_bytes, kLargeBlocks * blobfs::kBlobfsBlockSize);
// ... and we don't have space (as we try allocating).
fbl::unique_fd fd(open(info->path, O_CREAT | O_RDWR));
ASSERT_TRUE(fd);
ASSERT_EQ(-1, ftruncate(fd.get(), info->size_data));
ASSERT_EQ(ENOSPC, errno, "Blobfs expected to be out of space");
// Unlink all small blobs -- except for the last one, since we may have free
// trailing space at the end.
for (size_t i = 0; i < small_blobs.size() - 1; i++) {
ASSERT_EQ(0, unlink(small_blobs[i].c_str()), "Unlinking old blob");
}
// This asserts an assumption of our test: Freeing these blobs should provide
// enough space.
ASSERT_GT(kSmallSize * (small_blobs.size() - 1), kLargeSize);
// Validate that we have enough space (before we try allocating)...
ASSERT_NO_FAILURES(test->GetFsInfo(&usage));
ASSERT_GE(usage.total_bytes - usage.used_bytes, kLargeBlocks * blobfs::kBlobfsBlockSize);
// Now that blobfs supports extents, verify that we can still allocate a large
// blob, even if it is fragmented.
ASSERT_EQ(0, ftruncate(fd.get(), info->size_data));
// Sanity check that we can write and read the fragmented blob.
ASSERT_EQ(0, fs_test_utils::StreamAll(write, fd.get(), info->data.get(), info->size_data));
std::unique_ptr<char[]> buf(new char[info->size_data]);
ASSERT_EQ(0, lseek(fd.get(), 0, SEEK_SET));
ASSERT_EQ(0, fs_test_utils::StreamAll(read, fd.get(), buf.get(), info->size_data));
ASSERT_BYTES_EQ(info->data.get(), buf.get(), info->size_data);
// Sanity check that we can re-open and unlink the fragmented blob.
fd.reset(open(info->path, O_RDONLY));
ASSERT_TRUE(fd);
ASSERT_EQ(0, unlink(info->path));
}
TEST_F(BlobfsTest, Fragmentation) { RunFragmentationTest(this); }
TEST_F(BlobfsTestWithFvm, Fragmentation) { RunFragmentationTest(this); }
/*
typedef struct reopen_data {
char path[PATH_MAX];
std::atomic_bool complete;
} reopen_data_t;
int reopen_thread(void* arg) {
reopen_data_t* dat = static_cast<reopen_data_t*>(arg);
unsigned attempts = 0;
while (!atomic_load(&dat->complete)) {
fbl::unique_fd fd(open(dat->path, O_RDONLY));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
attempts++;
}
printf("Reopened %u times\n", attempts);
return 0;
}
// The purpose of this test is to repro the case where a blob is being retrieved from the blob hash
// at the same time it is being destructed, causing an invalid vnode to be returned. This can only
// occur when the client is opening a new fd to the blob at the same time it is being destructed
// after all writes to disk have completed.
// This test works best if a sleep is added at the beginning of fbl_recycle in VnodeBlob.
static bool CreateWriteReopen(BlobfsTest* blobfsTest) {
BEGIN_HELPER;
size_t num_ops = 10;
std::unique_ptr<fs_test_utils::BlobInfo> anchor_info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(MOUNT_PATH, 1 << 10, &anchor_info));
std::unique_ptr<fs_test_utils::BlobInfo> info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(MOUNT_PATH, 10 * (1 << 20), &info));
reopen_data_t dat;
strcpy(dat.path, info->path);
for (size_t i = 0; i < num_ops; i++) {
printf("Running op %lu... ", i);
fbl::unique_fd fd;
fbl::unique_fd anchor_fd;
atomic_store(&dat.complete, false);
// Write both blobs to disk (without verification, so we can start reopening the blob asap)
ASSERT_TRUE(MakeBlobUnverified(info.get(), &fd));
ASSERT_TRUE(MakeBlobUnverified(anchor_info.get(), &anchor_fd));
ASSERT_EQ(close(fd.release()), 0);
int result;
int success;
thrd_t thread;
ASSERT_EQ(thrd_create(&thread, reopen_thread, &dat), thrd_success);
{
// In case the test fails, always join the thread before returning from the test.
auto join_thread = fbl::MakeAutoCall([&]() {
atomic_store(&dat.complete, true);
success = thrd_join(thread, &result);
});
// Sleep while the thread continually opens and closes the blob
usleep(1000000);
ASSERT_EQ(syncfs(anchor_fd.get()), 0);
}
ASSERT_EQ(success, thrd_success);
ASSERT_EQ(result, 0);
ASSERT_EQ(close(anchor_fd.release()), 0);
ASSERT_EQ(unlink(info->path), 0);
ASSERT_EQ(unlink(anchor_info->path), 0);
}
END_HELPER;
}
*/
void RunCreateFailureTest(const RamDisk* disk, FilesystemTest* test) {
std::unique_ptr<fs_test_utils::BlobInfo> info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath, blobfs::kBlobfsBlockSize, &info));
// Attempt to create a blob, failing after each written block until the operations succeeds.
// After each failure, check for disk consistency.
fbl::unique_fd fd;
for (uint32_t blocks = 0; !fd; blocks++) {
ASSERT_OK(disk->SleepAfter(blocks));
// Blob creation may or may not succeed - as long as fsck passes, it doesn't matter.
MakeBlob(info.get(), &fd);
ASSERT_NO_FAILURES();
// Resolve all transactions before waking the ramdisk.
syncfs(fd.get());
ASSERT_OK(disk->WakeUp());
// Remount to check fsck results.
ASSERT_NO_FAILURES(test->Remount());
// Once file creation is successful, break out of the loop.
fd.reset(open(info->path, O_RDONLY));
}
}
TEST_F(BlobfsTest, CreateFailure) { RunCreateFailureTest(environment_->ramdisk(), this); }
TEST_F(BlobfsTestWithFvm, CreateFailure) { RunCreateFailureTest(environment_->ramdisk(), this); }
// Creates a new blob but (mostly) without complaining about failures.
void RelaxedMakeBlob(const fs_test_utils::BlobInfo* info, fbl::unique_fd* fd) {
fd->reset(open(info->path, O_CREAT | O_RDWR));
ASSERT_TRUE(*fd);
if (ftruncate(fd->get(), info->size_data) < 0) {
return;
}
fs_test_utils::StreamAll(write, fd->get(), info->data.get(), info->size_data);
}
TEST_F(BlobfsTestWithFvm, ExtendFailure) {
const RamDisk* ramdisk = environment_->ramdisk();
if (!ramdisk) {
return;
}
fuchsia_io_FilesystemInfo original_usage;
ASSERT_NO_FAILURES(GetFsInfo(&original_usage));
// Create a blob of the maximum size possible without causing an FVM extension.
std::unique_ptr<fs_test_utils::BlobInfo> old_info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(
kMountPath, original_usage.total_bytes - blobfs::kBlobfsBlockSize, &old_info));
fbl::unique_fd fd;
ASSERT_NO_FAILURES(MakeBlob(old_info.get(), &fd));
ASSERT_EQ(syncfs(fd.get()), 0);
fd.reset();
// Ensure that an FVM extension did not occur.
fuchsia_io_FilesystemInfo current_usage;
ASSERT_NO_FAILURES(GetFsInfo(&current_usage));
ASSERT_EQ(current_usage.total_bytes, original_usage.total_bytes);
// Generate another blob of the smallest size possible.
std::unique_ptr<fs_test_utils::BlobInfo> new_info;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath, blobfs::kBlobfsBlockSize, &new_info));
// Since the FVM metadata covers a large range of blocks, it will take a while to test a
// ramdisk failure after each individual block. Since we mostly care about what happens with
// blobfs after the extension succeeds on the FVM side, test a maximum of |metadata_failures|
// failures within the FVM metadata write itself.
size_t metadata_size = fvm::MetadataSize(environment_->disk_size(), kTestFvmSliceSize);
uint32_t metadata_blocks = static_cast<uint32_t>(metadata_size / ramdisk->page_size());
uint32_t metadata_failures = 16;
uint32_t increment = metadata_blocks / fbl::min(metadata_failures, metadata_blocks);
// Round down the metadata block count so we don't miss testing the transaction immediately
// after the metadata write succeeds.
metadata_blocks = fbl::round_down(metadata_blocks, increment);
uint32_t blocks = 0;
while (true) {
ASSERT_OK(ramdisk->SleepAfter(blocks));
// Blob creation may or may not succeed - as long as fsck passes, it doesn't matter.
RelaxedMakeBlob(new_info.get(), &fd);
// Resolve all transactions before waking the ramdisk.
syncfs(fd.get());
ASSERT_OK(ramdisk->WakeUp());
// Replay the journal.
Unmount();
ASSERT_NO_FAILURES(Mount());
// Remount again to verify integrity.
ASSERT_NO_FAILURES(Remount());
// Check that the original blob still exists.
fd.reset(open(old_info->path, O_RDONLY));
ASSERT_TRUE(fd);
// Once file creation is successful, break out of the loop.
fd.reset(open(new_info->path, O_RDONLY));
if (fd) {
struct stat stats;
ASSERT_EQ(fstat(fd.get(), &stats), 0);
ASSERT_EQ(static_cast<uint64_t>(stats.st_size), old_info->size_data);
break;
}
if (blocks >= metadata_blocks) {
blocks++;
} else {
blocks += increment;
}
}
// Ensure that an FVM extension occurred.
ASSERT_NO_FAILURES(GetFsInfo(&current_usage));
ASSERT_GT(current_usage.total_bytes, original_usage.total_bytes);
}
class LargeBlobTest : public BlobfsFixedDiskSizeTest {
public:
LargeBlobTest() : BlobfsFixedDiskSizeTest(GetDiskSize()) {}
static uint64_t GetDataBlockCount() { return 12 * blobfs::kBlobfsBlockBits / 10; }
private:
static uint64_t GetDiskSize() {
// Create blobfs with enough data blocks to ensure 2 block bitmap blocks.
// Any number above kBlobfsBlockBits should do, and the larger the
// number, the bigger the disk (and memory used for the test).
blobfs::Superblock superblock;
superblock.flags = 0;
superblock.inode_count = blobfs::kBlobfsDefaultInodeCount;
superblock.journal_block_count = blobfs::kDefaultJournalBlocks;
superblock.data_block_count = GetDataBlockCount();
return blobfs::TotalBlocks(superblock) * blobfs::kBlobfsBlockSize;
}
};
TEST_F(LargeBlobTest, UseSecondBitmap) {
// Create (and delete) a blob large enough to overflow into the second bitmap block.
std::unique_ptr<fs_test_utils::BlobInfo> info;
size_t blob_size = ((GetDataBlockCount() / 2) + 1) * blobfs::kBlobfsBlockSize;
ASSERT_TRUE(fs_test_utils::GenerateRandomBlob(kMountPath, blob_size, &info));
fbl::unique_fd fd;
ASSERT_NO_FAILURES(MakeBlob(info.get(), &fd));
ASSERT_EQ(syncfs(fd.get()), 0);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_EQ(unlink(info->path), 0);
}
} // namespace
/*
BEGIN_TEST_CASE(blobfs_tests)
RUN_TESTS(LARGE, CreateUmountRemountLarge)
RUN_TESTS(LARGE, CreateUmountRemountLargeMultithreaded)
RUN_TESTS(LARGE, CreateWriteReopen)
END_TEST_CASE(blobfs_tests)
*/