blob: 70118bcdd00ce6752abbd9384967ae9d4d1add56 [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.
// Tests for MinFS-specific behavior.
#include <fcntl.h>
#include <fidl/fuchsia.fs/cpp/wire.h>
#include <fidl/fuchsia.hardware.block.volume/cpp/wire.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/vfs.h>
#include <lib/zx/vmo.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <algorithm>
#include <string>
#include <vector>
#include <fbl/algorithm.h>
#include <fbl/array.h>
#include <fbl/unique_fd.h>
#include "src/storage/fs_test/fs_test_fixture.h"
#include "src/storage/fvm/format.h"
#include "src/storage/minfs/format.h"
#include "src/storage/minfs/transaction_limits.h"
namespace fs_test {
namespace {
namespace fio = fuchsia_io;
// Tests using MinfsTest will get tested with and without FVM.
using MinfsTest = FilesystemTest;
void QueryInfo(const TestFilesystem& fs, fio::wire::FilesystemInfo* info) {
// Sync before querying fs so that we can obtain an accurate number of used bytes. Otherwise,
// blocks which are reserved but not yet allocated won't be counted.
fbl::unique_fd root_fd = fs.GetRootFd();
fsync(root_fd.get());
fdio_cpp::FdioCaller caller(std::move(root_fd));
auto result = fidl::WireCall(caller.borrow_as<fio::Directory>())->QueryFilesystem();
ASSERT_EQ(result.status(), ZX_OK);
ASSERT_EQ(result->s, ZX_OK);
ASSERT_NE(result->info, nullptr);
*info = *(result->info);
// For now, info->name.data is a fixed size array.
const char* kFsName = "minfs";
ASSERT_EQ(memcmp(info->name.data(), kFsName, strlen(kFsName) + 1), 0)
<< "Unexpected filesystem mounted";
ASSERT_EQ(info->block_size, minfs::kMinfsBlockSize);
ASSERT_EQ(info->max_filename_size, minfs::kMinfsMaxNameSize);
ASSERT_EQ(info->fs_type, fidl::ToUnderlying(fuchsia_fs::VfsType::kMinfs));
ASSERT_NE(info->fs_id, 0ul);
ASSERT_EQ(info->used_bytes % info->block_size, 0ul);
ASSERT_EQ(info->total_bytes % info->block_size, 0ul);
ASSERT_EQ(info->free_shared_pool_bytes % info->block_size, 0ul);
}
void GetFreeBlocks(const TestFilesystem& fs, uint32_t* out_free_blocks) {
fio::wire::FilesystemInfo info;
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs, &info));
uint64_t total_bytes = info.total_bytes + info.free_shared_pool_bytes;
uint64_t used_bytes = info.used_bytes;
*out_free_blocks = static_cast<uint32_t>((total_bytes - used_bytes) / info.block_size);
}
// Write to the file until at most |max_remaining_blocks| remain in the partition.
// Return the new remaining block count as |actual_remaining_blocks|.
void FillPartition(const TestFilesystem& fs, int fd, uint32_t max_remaining_blocks,
uint32_t* actual_remaining_blocks) {
std::vector data(1'048'576, 0xaa);
uint32_t free_blocks;
while (true) {
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs, &free_blocks));
if (free_blocks <= max_remaining_blocks) {
break;
}
uint32_t blocks = free_blocks - max_remaining_blocks;
// Assume that writing 1 block might require writing 2 additional indirect blocks, so if there
// are more than 2 blocks to go, subtract 2, and if there are only 2 blocks to go, only do 1
// block.
if (blocks > 2) {
blocks -= 2;
} else if (blocks == 2) {
--blocks;
}
size_t bytes = std::min(data.size(), static_cast<size_t>(blocks) * minfs::kMinfsBlockSize);
ASSERT_EQ(write(fd, data.data(), bytes), static_cast<ssize_t>(bytes));
}
ASSERT_LE(free_blocks, max_remaining_blocks);
*actual_remaining_blocks = free_blocks;
}
// Tests using MinfsFvmTest will only run with FVM.
class MinfsFvmTest : public BaseFilesystemTest {
public:
explicit MinfsFvmTest(
const TestFilesystemOptions& options = OptionsWithDescription("MinfsWithFvm"))
: BaseFilesystemTest(options) {}
protected:
zx::result<fidl::ClientEnd<fuchsia_hardware_block_volume::VolumeManager>> GetVolumeManager() {
// This expects to be set up with a RamDisk. The filesystem has some variants and this could be
// on a RamNand, but then this code would need updating.
storage::RamDisk* ram_disk = fs().GetRamDisk();
if (ram_disk == nullptr) {
return zx::error(ZX_ERR_BAD_STATE);
}
// Want something like "/dev/sys/platform/ram-disk/ramctl/ramdisk-0/block/fvm"
std::string fvm_path = ram_disk->path() + "/fvm";
return component::Connect<fuchsia_hardware_block_volume::VolumeManager>(fvm_path);
}
// Returns the GUID associated with the minfs partition inside FVM.
zx::result<fuchsia_hardware_block_partition::wire::Guid> GetMinfsPartitionGuid() {
zx::result<std::string> device_path = fs().DevicePath();
if (device_path.is_error()) {
return device_path.take_error();
}
zx::result partition =
component::Connect<fuchsia_hardware_block_partition::Partition>(device_path.value());
if (partition.is_error()) {
return partition.take_error();
}
fidl::WireResult response = fidl::WireCall(partition.value())->GetInstanceGuid();
if (response.status() != ZX_OK)
return zx::error(response.status());
if (response.value().status != ZX_OK)
return zx::error(response.value().status);
return zx::ok(*response.value().guid);
}
zx::result<fuchsia_hardware_block_volume::wire::VolumeManagerInfo> GetVolumeManagerInfo() {
zx::result volume_manager = GetVolumeManager();
if (volume_manager.is_error()) {
return volume_manager.take_error();
}
fidl::WireResult response = fidl::WireCall(volume_manager.value())->GetInfo();
if (response.status() != ZX_OK)
return zx::error(response.status());
if (response.value().status != ZX_OK)
return zx::error(response.value().status);
return zx::ok(*response.value().info);
}
zx_status_t SetPartitionLimit(uint64_t slice_limit) {
zx::result volume_manager = GetVolumeManager();
if (volume_manager.is_error()) {
return volume_manager.error_value();
}
auto guid_or = GetMinfsPartitionGuid();
EXPECT_TRUE(guid_or.is_ok());
fidl::WireResult set_response =
fidl::WireCall(volume_manager.value())->SetPartitionLimit(*guid_or, slice_limit);
if (set_response.status() != ZX_OK)
return set_response.status();
if (set_response.value().status != ZX_OK)
return set_response.value().status;
// Query the partition limit to make sure it worked.
fidl::WireResult get_response =
fidl::WireCall(volume_manager.value())->GetPartitionLimit(*guid_or);
if (get_response.status() != ZX_OK)
return get_response.status();
if (get_response.value().status != ZX_OK)
return get_response.value().status;
EXPECT_EQ(slice_limit, get_response.value().slice_count);
return ZX_OK;
}
};
class MinfsFvmTestWith8MiBSliceSize : public MinfsFvmTest {
public:
static constexpr uint64_t kSliceSize{UINT64_C(1024) * 1024 * 8};
static TestFilesystemOptions GetOptions() {
auto options = OptionsWithDescription("MinfsWithFvm");
options.fvm_slice_size = kSliceSize;
return options;
}
MinfsFvmTestWith8MiBSliceSize() : MinfsFvmTest(GetOptions()) {}
};
// Tests using MinfsWithoutFvmTest will only run without FVM.
class MinfsWithoutFvmTest : public BaseFilesystemTest {
public:
MinfsWithoutFvmTest() : BaseFilesystemTest(OptionsWithDescription("MinfsWithoutFvm")) {}
protected:
void GetAllocatedBlocks(uint64_t* out_allocated_blocks) const {
fio::wire::FilesystemInfo info;
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
*out_allocated_blocks = static_cast<uint64_t>(info.used_bytes) / info.block_size;
}
};
// Verify initial conditions on a filesystem, and validate that filesystem modifications adjust the
// query info accordingly.
TEST_F(MinfsFvmTest, QueryInitialState) {
fio::wire::FilesystemInfo info;
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
EXPECT_EQ(fs().options().fvm_slice_size, info.total_bytes);
// TODO(https://fxbug.dev/42106232): Adjust this once minfs accounting on truncate is fixed.
EXPECT_EQ(2 * minfs::kMinfsBlockSize, info.used_bytes);
// The inodes will use the required slices (rounded up). Since the current default inode data size
// divides evenly into the slice size, the values should match exactly.
EXPECT_EQ(fs().options().fvm_slice_size, 32768u); // Verify expectations of this test.
EXPECT_EQ(minfs::kMinfsDefaultInodeCount, info.total_nodes);
// The "zero-th" inode is reserved, as well as the root directory.
const uint64_t kInitialUsedNodes = 2;
EXPECT_EQ(kInitialUsedNodes, info.used_nodes);
// Allocate kExtraNodeCount new files, each using truncated (sparse) files.
const uint64_t kExtraNodeCount = 16;
for (uint64_t i = 0; i < kExtraNodeCount; i++) {
const std::string path = GetPath("file_" + std::to_string(i));
fbl::unique_fd fd(open(path.c_str(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_GT(fd.get(), 0);
ASSERT_EQ(ftruncate(fd.get(), static_cast<off_t>(30) * 1024), 0);
}
// Adjust our query expectations: We should see the new nodes.
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
EXPECT_EQ(kInitialUsedNodes + kExtraNodeCount, info.used_nodes);
}
// Return number of blocks allocated by the file at |fd|.
void GetFileBlocks(int fd, uint64_t* blocks) {
struct stat stats;
ASSERT_EQ(fstat(fd, &stats), 0);
off_t size = stats.st_blocks * VNATTR_BLKSIZE;
ASSERT_EQ(size % minfs::kMinfsBlockSize, 0);
*blocks = static_cast<uint64_t>(size / minfs::kMinfsBlockSize);
}
// Fill a directory to at most |max_blocks| full of direntries.
// We assume the directory is empty to begin with, and any files we are adding do not already exist.
void FillDirectory(const TestFilesystem& fs, int dir_fd, uint32_t max_blocks) {
uint32_t file_count = 0;
int entries_per_iteration = 150;
while (true) {
std::string path;
for (int i = 0; i < entries_per_iteration; ++i) {
path = "file_" + std::to_string(file_count++);
fbl::unique_fd fd(openat(dir_fd, path.c_str(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
}
uint64_t current_blocks;
ASSERT_NO_FATAL_FAILURE(GetFileBlocks(dir_fd, &current_blocks));
if (current_blocks > max_blocks) {
ASSERT_EQ(unlinkat(dir_fd, path.c_str(), 0), 0);
break;
}
if (current_blocks == max_blocks) {
// Do just one entry per iteration for the last block.
entries_per_iteration = 1;
}
}
}
TEST_F(MinfsFvmTestWith8MiBSliceSize, FreeSharedPoolBytes) {
// Get the volume initial conditions for computing what minfs should be returning. There should be
// at least two free slices for us to test the partition limit.
zx::result<fuchsia_hardware_block_volume::wire::VolumeManagerInfo> manager_info =
GetVolumeManagerInfo();
ASSERT_TRUE(manager_info.is_ok());
ASSERT_LT(manager_info->assigned_slice_count, manager_info->slice_count);
uint64_t free_slices = manager_info->slice_count - manager_info->assigned_slice_count;
// Normal free space size should just report the volume manager's free space.
fio::wire::FilesystemInfo info;
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
ASSERT_EQ(kSliceSize * free_slices, info.free_shared_pool_bytes);
// Lower the partition limit to one more slice than the filesystem currently is using (since
// there's only our partition in FVM, we know all used slices belong to minfs).
uint64_t new_limit = manager_info->assigned_slice_count + 1;
ASSERT_EQ(ZX_OK, SetPartitionLimit(new_limit));
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
EXPECT_EQ(kSliceSize, info.free_shared_pool_bytes); // Set exactly one slice free.
// Match the limit to the current partition size.
new_limit = manager_info->assigned_slice_count;
ASSERT_EQ(ZX_OK, SetPartitionLimit(new_limit));
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
EXPECT_EQ(0u, info.free_shared_pool_bytes); // No slices free.
// Lower the limit to to below the partition size.
new_limit = manager_info->assigned_slice_count - 1;
ASSERT_EQ(ZX_OK, SetPartitionLimit(new_limit));
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
EXPECT_EQ(0u, info.free_shared_pool_bytes); // No slices free.
// Remove the limit, it should go back to the full free bytes.
ASSERT_EQ(ZX_OK, SetPartitionLimit(0u));
ASSERT_NO_FATAL_FAILURE(QueryInfo(fs(), &info));
ASSERT_EQ(kSliceSize * free_slices, info.free_shared_pool_bytes);
}
// Test various operations when the Minfs partition is near capacity. This test is sensitive to the
// FVM slice size and was designed with a 8 MiB slice size in mind.
TEST_F(MinfsFvmTestWith8MiBSliceSize, FullOperations) {
// Define file and directory names we will use upfront.
const char* big_path = "big_file";
const char* med_path = "med_file";
const char* sml_path = "sml_file";
const char* dir_path = "directory";
// Open the mount point and create three files.
fbl::unique_fd mnt_fd = fs().GetRootFd();
ASSERT_TRUE(mnt_fd);
fbl::unique_fd big_fd(openat(mnt_fd.get(), big_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_TRUE(big_fd);
fbl::unique_fd med_fd(openat(mnt_fd.get(), med_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_TRUE(med_fd);
fbl::unique_fd sml_fd(openat(mnt_fd.get(), sml_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_TRUE(sml_fd);
// Write to the big file, filling the partition and leaving 2 blocks unused.
uint32_t free_blocks = 2;
uint32_t actual_blocks;
ASSERT_NO_FATAL_FAILURE(FillPartition(fs(), big_fd.get(), free_blocks, &actual_blocks));
// Delete the big file.
big_fd.reset();
ASSERT_EQ(unlinkat(mnt_fd.get(), big_path, 0), 0);
// Try to write to more than the previously available blocks, which should succeed if the big
// file's blocks were reclaimed properly.
auto data = std::vector(static_cast<size_t>(minfs::kMinfsBlockSize) * (actual_blocks + 1), 0xaa);
ASSERT_EQ(write(med_fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size()));
// Remove all of the data in the medium file.
ASSERT_EQ(ftruncate(med_fd.get(), 0), 0);
ASSERT_EQ(lseek(med_fd.get(), 0, SEEK_SET), 0);
// Recreate the big file
big_fd.reset(openat(mnt_fd.get(), big_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_TRUE(big_fd);
// Write to the big file, filling the partition and leaving at most kMinfsDirect + 1 blocks
// unused.
free_blocks = minfs::kMinfsDirect + 1;
ASSERT_NO_FATAL_FAILURE(FillPartition(fs(), big_fd.get(), free_blocks, &actual_blocks));
// Write enough data to the second file to take up all remaining blocks except for 1.
// This should strictly be writing to the direct block section of the file.
data = std::vector(static_cast<size_t>(minfs::kMinfsBlockSize), 0xaa);
ASSERT_EQ(data.size(), static_cast<size_t>(minfs::kMinfsBlockSize));
for (unsigned i = 0; i < actual_blocks - 1; i++) {
ASSERT_EQ(write(med_fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size()));
}
// Make sure we now have only 1 block remaining.
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &free_blocks));
ASSERT_EQ(free_blocks, 1u);
// We should now have exactly 1 free block remaining. Attempt to write into the indirect
// section of the file so we ensure that at least 2 blocks are required.
// This is expected to fail.
ASSERT_EQ(lseek(med_fd.get(), static_cast<off_t>(minfs::kMinfsBlockSize) * minfs::kMinfsDirect,
SEEK_SET),
minfs::kMinfsBlockSize * minfs::kMinfsDirect);
ASSERT_EQ(data.size(), static_cast<size_t>(minfs::kMinfsBlockSize));
ASSERT_LT(write(med_fd.get(), data.data(), data.size()), 0);
// We should still have 1 free block remaining. Writing to the beginning of the second file
// should only require 1 (direct) block, and therefore pass.
// Note: This fails without block reservation.
ASSERT_EQ(data.size(), static_cast<size_t>(minfs::kMinfsBlockSize));
ASSERT_EQ(write(sml_fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size()));
// There are no longer any blocks free.
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &free_blocks));
ASSERT_EQ(free_blocks, 0u);
// Making directory should fail now that the file system is completely full.
ASSERT_LT(mkdirat(mnt_fd.get(), dir_path, 0666), 0);
// Remove the small file, which should free up blocks.
sml_fd.reset();
ASSERT_EQ(unlinkat(mnt_fd.get(), sml_path, 0), 0);
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &free_blocks));
ASSERT_GT(free_blocks, 0u);
// Without block reservation, something from the failed write remains allocated. Try editing
// nearby blocks to force a writeback of partially allocated data.
// Note: This will fail without block reservation since the previous failed write would leave
// the only free block incorrectly allocated and 1 additional block is required for
// copy-on-write truncation.
struct stat s;
ASSERT_EQ(fstat(big_fd.get(), &s), 0);
auto truncate_size = static_cast<ssize_t>(
fbl::round_up(static_cast<uint64_t>(s.st_size / 2), minfs::kMinfsBlockSize));
ASSERT_EQ(ftruncate(big_fd.get(), truncate_size), 0);
// Attempt to remount. Without block reservation, an additional block from the previously
// failed write will still be incorrectly allocated, causing fsck to fail.
EXPECT_EQ(fs().Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs().Fsck().status_value(), ZX_OK);
EXPECT_EQ(fs().Mount().status_value(), ZX_OK);
mnt_fd = fs().GetRootFd();
ASSERT_TRUE(mnt_fd);
// Re-open big file
big_fd.reset(openat(mnt_fd.get(), big_path, O_RDWR));
ASSERT_TRUE(big_fd);
// Re-create the small file
sml_fd.reset(openat(mnt_fd.get(), sml_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_TRUE(sml_fd);
// Make sure we now have at least kMinfsDirect + 1 blocks remaining.
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &free_blocks));
ASSERT_GE(free_blocks, minfs::kMinfsDirect + 1);
// We have some room now, so create a new directory.
ASSERT_EQ(mkdirat(mnt_fd.get(), dir_path, 0666), 0);
fbl::unique_fd dir_fd(openat(mnt_fd.get(), dir_path, O_RDONLY));
ASSERT_TRUE(dir_fd);
// Fill the directory up to kMinfsDirect blocks full of direntries.
ASSERT_NO_FATAL_FAILURE(FillDirectory(fs(), dir_fd.get(), minfs::kMinfsDirect));
// Now re-fill the partition by writing as much as possible back to the original file.
// Attempt to leave 1 block free.
ASSERT_EQ(lseek(big_fd.get(), truncate_size, SEEK_SET), truncate_size);
free_blocks = 1;
ASSERT_NO_FATAL_FAILURE(FillPartition(fs(), big_fd.get(), free_blocks, &actual_blocks));
if (actual_blocks == 0) {
// It is possible that, in our previous allocation of big_fd, we ended up leaving less than
// |free_blocks| free. Since the file has grown potentially large, it is possible that
// allocating a single block will also allocate additional indirect blocks.
// For example, in a case where we have 2 free blocks remaining and expect to allocate 1,
// we may actually end up allocating 2 instead, leaving us with 0 free blocks.
// Since sml_fd is using less than kMinfsDirect blocks and thus is guaranteed to have a 1:1
// block usage ratio, we can remedy this situation by removing a single block from sml_fd.
ASSERT_EQ(ftruncate(sml_fd.get(), 0), 0);
}
while (actual_blocks > free_blocks) {
// Otherwise, if too many blocks remain (if e.g. we needed to allocate 3 blocks but only 2
// are remaining), write to sml_fd until only 1 remains.
ASSERT_EQ(data.size(), static_cast<size_t>(minfs::kMinfsBlockSize));
ASSERT_EQ(write(sml_fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size()));
actual_blocks--;
}
// Ensure that there is now exactly one block remaining.
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &actual_blocks));
ASSERT_EQ(free_blocks, actual_blocks);
// Now, attempt to add one more file to the directory we created. Since it will need to
// allocate 2 blocks (1 indirect + 1 direct) and there is only 1 remaining, it should fail.
uint64_t block_count;
ASSERT_NO_FATAL_FAILURE(GetFileBlocks(dir_fd.get(), &block_count));
ASSERT_EQ(block_count, minfs::kMinfsDirect);
fbl::unique_fd tmp_fd(openat(dir_fd.get(), "new_file", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
ASSERT_FALSE(tmp_fd);
// Again, try editing nearby blocks to force bad allocation leftovers to be persisted, and
// remount the partition. This is expected to fail without block reservation.
ASSERT_EQ(fstat(big_fd.get(), &s), 0);
ASSERT_EQ(s.st_size % minfs::kMinfsBlockSize, 0);
truncate_size = s.st_size - minfs::kMinfsBlockSize;
ASSERT_EQ(ftruncate(big_fd.get(), truncate_size), 0);
EXPECT_EQ(fs().Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs().Fsck().status_value(), ZX_OK);
EXPECT_EQ(fs().Mount().status_value(), ZX_OK);
// Re-open files.
mnt_fd = fs().GetRootFd();
ASSERT_TRUE(mnt_fd);
big_fd.reset(openat(mnt_fd.get(), big_path, O_RDWR));
ASSERT_TRUE(big_fd);
sml_fd.reset(openat(mnt_fd.get(), sml_path, O_RDWR));
ASSERT_TRUE(sml_fd);
// Fill the partition again, writing one block of data to sml_fd
// in case we need an emergency truncate.
ASSERT_EQ(data.size(), static_cast<size_t>(minfs::kMinfsBlockSize));
ASSERT_EQ(write(sml_fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size()));
ASSERT_EQ(lseek(big_fd.get(), truncate_size, SEEK_SET), truncate_size);
free_blocks = 1;
ASSERT_NO_FATAL_FAILURE(FillPartition(fs(), big_fd.get(), free_blocks, &actual_blocks));
if (actual_blocks == 0) {
// If we ended up with fewer blocks than expected, truncate sml_fd to create more space.
// (See note above for details.)
ASSERT_EQ(ftruncate(sml_fd.get(), 0), 0);
}
ASSERT_EQ(data.size(), static_cast<size_t>(minfs::kMinfsBlockSize));
while (actual_blocks > free_blocks) {
// Otherwise, if too many blocks remain (if e.g. we needed to allocate 3 blocks but only 2
// are remaining), write to sml_fd until only 1 remains.
ASSERT_EQ(write(sml_fd.get(), data.data(), data.size()), static_cast<ssize_t>(data.size()));
actual_blocks--;
}
// Ensure that there is now exactly one block remaining.
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &actual_blocks));
ASSERT_EQ(free_blocks, actual_blocks);
// Now, attempt to rename one of our original files under the new directory.
// This should also fail.
ASSERT_NE(renameat(mnt_fd.get(), med_path, dir_fd.get(), med_path), 0);
// Again, truncate the original file and attempt to remount.
// Again, this should fail without block reservation.
ASSERT_EQ(fstat(big_fd.get(), &s), 0);
ASSERT_EQ(s.st_size % minfs::kMinfsBlockSize, 0);
truncate_size = s.st_size - minfs::kMinfsBlockSize;
ASSERT_EQ(ftruncate(big_fd.get(), truncate_size), 0);
EXPECT_EQ(fs().Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs().Mount().status_value(), ZX_OK);
mnt_fd = fs().GetRootFd();
ASSERT_EQ(unlinkat(mnt_fd.get(), big_path, 0), 0);
ASSERT_EQ(unlinkat(mnt_fd.get(), med_path, 0), 0);
ASSERT_EQ(unlinkat(mnt_fd.get(), sml_path, 0), 0);
}
TEST_P(MinfsTest, UnlinkFail) {
uint32_t original_blocks;
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &original_blocks));
uint32_t fd_count = 100;
fbl::unique_fd fds[fd_count];
char data[minfs::kMinfsBlockSize];
memset(data, 0xaa, sizeof(data));
const std::string filename = GetPath("file");
// Open, write to, and unlink |fd_count| total files without closing them.
for (unsigned i = 0; i < fd_count; i++) {
// Since we are unlinking, we can use the same filename for all files.
fds[i].reset(open(filename.c_str(), O_CREAT | O_RDWR | O_EXCL, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fds[i]);
ASSERT_EQ(write(fds[i].get(), data, sizeof(data)), static_cast<ssize_t>(sizeof(data)));
ASSERT_EQ(unlink(filename.c_str()), 0);
}
// Close the first, middle, and last files to test behavior when various "links" are removed.
uint32_t first_fd = 0;
uint32_t mid_fd = fd_count / 2;
uint32_t last_fd = fd_count - 1;
ASSERT_EQ(close(fds[first_fd].release()), 0);
ASSERT_EQ(close(fds[mid_fd].release()), 0);
ASSERT_EQ(close(fds[last_fd].release()), 0);
// Sync Minfs to ensure all unlink operations complete.
fbl::unique_fd fd(open(filename.c_str(), O_CREAT, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
ASSERT_EQ(syncfs(fd.get()), 0);
// Check that the number of Minfs free blocks has decreased.
uint32_t current_blocks;
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &current_blocks));
ASSERT_LT(current_blocks, original_blocks);
// Put the ramdisk to sleep and close all the fds. This will cause file purge to fail,
// and all unlinked files will be left intact (on disk).
ASSERT_EQ(fs().GetRamDisk()->SleepAfter(0).status_value(), ZX_OK);
// The ram-disk is asleep but since no transactions have been processed, the writeback state has
// not been updated. The first file we close will appear to succeed.
ASSERT_EQ(close(fds[first_fd + 1].release()), 0);
// Sync to ensure the writeback state is updated. Since the purge from the previous close will
// fail, sync will also fail.
ASSERT_LT(syncfs(fd.get()), 0);
// Close all open fds.
for (unsigned i = first_fd + 2; i < last_fd; i++) {
if (i != mid_fd) {
ASSERT_EQ(close(fds[i].release()), -1);
}
}
// Sync Minfs to ensure all close operations complete. Since Minfs is in a read-only state and
// some requests have not been successfully persisted to disk, the sync is expected to fail.
ASSERT_LT(syncfs(fd.get()), 0);
// Remount Minfs, which should cause leftover unlinked files to be removed.
ASSERT_EQ(fs().GetRamDisk()->Wake().status_value(), ZX_OK);
EXPECT_EQ(fs().Unmount().status_value(), ZX_OK);
EXPECT_EQ(fs().Mount().status_value(), ZX_OK);
// Check that the block count has been reverted to the value before any files were added.
ASSERT_NO_FATAL_FAILURE(GetFreeBlocks(fs(), &current_blocks));
ASSERT_EQ(current_blocks, original_blocks);
}
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, MinfsTest, testing::ValuesIn(AllTestFilesystems()),
testing::PrintToStringParamName());
} // namespace
} // namespace fs_test