| /* |
| * Copyright (C) 2015-2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include <assert.h> |
| #include <inttypes.h> |
| #include <limits.h> |
| #include <lk/macros.h> |
| #include <malloc.h> |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <string.h> |
| |
| #include <openssl/rand.h> |
| |
| #include "block_allocator.h" |
| #include "block_cache.h" |
| #include "block_map.h" |
| #include "block_set.h" |
| #include "checkpoint.h" |
| #include "crypt.h" |
| #include "debug_stats.h" |
| #include "error_reporting_mock.h" |
| #include "file.h" |
| #include "transaction.h" |
| |
| #include <time.h> |
| |
| long gettime(uint32_t clock_id, uint32_t flags, int64_t* time) { |
| int ret; |
| struct timespec ts; |
| assert(!clock_id); |
| assert(!flags); |
| |
| ret = clock_gettime(CLOCK_MONOTONIC, &ts); |
| assert(!ret); |
| *time = ts.tv_sec * 1000000000LL + ts.tv_nsec; |
| |
| return 0; |
| } |
| |
| #define FILE_SYSTEM_TEST "block_test" |
| |
| #if 0 /* test tree order 3 */ |
| /* not useful, b+tree for free set grows faster than the space that is added to it */ |
| #define BLOCK_SIZE (64) |
| #define BLOCK_COUNT (256) |
| #elif 0 /* test tree order 4 */ |
| #define BLOCK_SIZE (80) |
| #define BLOCK_COUNT (256) |
| #elif 0 /* test tree order 5 */ |
| #define BLOCK_SIZE (96) |
| #define BLOCK_COUNT (256) |
| #elif 0 /* test tree order 6 */ |
| #define BLOCK_SIZE (112) |
| #define BLOCK_COUNT (256) |
| #elif 0 /* test tree order 7 */ |
| #define BLOCK_SIZE (128) |
| #define BLOCK_COUNT (256) |
| #elif 0 /* test tree order 8 */ |
| #define BLOCK_SIZE (144) |
| #define BLOCK_COUNT (256) |
| #elif 0 /* test single rpmb block with 64-bit indexes */ |
| #define BLOCK_SIZE (256) |
| #define BLOCK_COUNT (256) |
| #elif 1 |
| #define BLOCK_SIZE (2048) |
| #define BLOCK_COUNT (256) |
| #elif 0 |
| /* test single rpmb block with simulated 16-bit indexes, 128kb device */ |
| #define BLOCK_SIZE (256 * 4) |
| #define BLOCK_COUNT (512) |
| #elif 0 |
| /* test single rpmb block with simulated 16-bit indexes, 4MB device */ |
| #define BLOCK_SIZE (256 * 4) |
| #define BLOCK_COUNT (16384) |
| #else |
| #define BLOCK_SIZE (256 * 4) |
| #define BLOCK_COUNT (0x10000) |
| #endif |
| |
| struct block { |
| char data[BLOCK_SIZE]; |
| char data_copy[BLOCK_SIZE]; |
| struct mac mac; |
| bool loaded; |
| bool dirty; |
| bool dirty_ref; |
| struct block* parent; |
| struct block_mac* block_mac_in_parent; |
| const char* used_by_str; |
| data_block_t used_by_block; |
| const char* checkpoint_used_by_str; |
| data_block_t checkpoint_used_by_block; |
| }; |
| static struct block blocks[BLOCK_COUNT]; |
| static struct block blocks_backup[BLOCK_COUNT]; |
| static const struct key key; |
| |
| static bool allow_repaired = false; |
| |
| static bool print_test_verbose = false; |
| static bool print_block_tree_test_verbose = false; |
| |
| data_block_t block_test_fail_write_blocks; |
| |
| static inline void transaction_complete(struct transaction* tr) { |
| return transaction_complete_etc(tr, false); |
| } |
| |
| static inline void transaction_complete_update_checkpoint( |
| struct transaction* tr) { |
| return transaction_complete_etc(tr, true); |
| } |
| |
| static void block_test_clear_reinit_etc(struct transaction* tr, |
| uint32_t flags, |
| bool swap, |
| bool clear, |
| size_t start) { |
| struct fs* fs = tr->fs; |
| const struct key* key = fs->key; |
| struct block_device* dev = tr->fs->dev; |
| struct block_device* super_dev = tr->fs->super_dev; |
| int i; |
| struct block tmp; |
| int ret; |
| |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| if (swap) { |
| for (i = start; i < BLOCK_COUNT; ++i) { |
| tmp = blocks[i]; |
| blocks[i] = blocks_backup[i]; |
| blocks_backup[i] = tmp; |
| } |
| } |
| |
| if (clear) { |
| memset(&blocks[start], 0, (BLOCK_COUNT - start) * sizeof(struct block)); |
| } |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, flags); |
| assert(ret == 0); |
| fs->reserved_count = 18; /* HACK: override default reserved space */ |
| transaction_init(tr, fs, true); |
| } |
| |
| static void block_test_swap_clear_reinit(struct transaction* tr, |
| uint32_t flags) { |
| block_test_clear_reinit_etc(tr, flags, true, true, 2); |
| } |
| |
| static void block_test_swap_reinit(struct transaction* tr, uint32_t flags) { |
| block_test_clear_reinit_etc(tr, flags, true, false, 2); |
| } |
| |
| static void block_test_reinit(struct transaction* tr, uint32_t flags) { |
| block_test_clear_reinit_etc(tr, flags, false, false, 2); |
| } |
| |
| static void block_test_clear_superblock_reinit(struct transaction* tr, |
| uint32_t flags) { |
| block_test_clear_reinit_etc(tr, flags, false, true, 0); |
| } |
| |
| static void block_test_start_read(struct block_device* dev, |
| data_block_t block) { |
| assert(dev->block_size <= BLOCK_SIZE); |
| assert(block < countof(blocks)); |
| block_cache_complete_read(dev, block, blocks[block].data, dev->block_size, |
| BLOCK_READ_SUCCESS); |
| } |
| |
| static void block_test_start_write(struct block_device* dev, |
| data_block_t block, |
| const void* data, |
| size_t data_size, |
| bool sync) { |
| assert(block < countof(blocks)); |
| assert(data_size <= sizeof(blocks[block].data)); |
| memcpy(blocks[block].data, data, data_size); |
| block_cache_complete_write(dev, block, |
| block < block_test_fail_write_blocks |
| ? BLOCK_WRITE_FAILED |
| : BLOCK_WRITE_SUCCESS); |
| } |
| |
| #if FULL_ASSERT |
| static void block_clear_used_by(void) { |
| size_t block; |
| for (block = 0; block < countof(blocks); block++) { |
| blocks[block].used_by_str = NULL; |
| blocks[block].used_by_block = 0; |
| blocks[block].checkpoint_used_by_str = NULL; |
| blocks[block].checkpoint_used_by_block = 0; |
| } |
| } |
| |
| static void block_set_used_by_etc(data_block_t block, |
| const char* used_by_str, |
| data_block_t used_by_block, |
| bool checkpoint, |
| bool force) { |
| assert(block < countof(blocks)); |
| |
| if (checkpoint) { |
| if (force || !blocks[block].checkpoint_used_by_str) { |
| blocks[block].checkpoint_used_by_str = used_by_str; |
| blocks[block].checkpoint_used_by_block = used_by_block; |
| } |
| assert(blocks[block].checkpoint_used_by_str == used_by_str); |
| assert(blocks[block].checkpoint_used_by_block == used_by_block); |
| } else { |
| if (force || !blocks[block].used_by_str) { |
| blocks[block].used_by_str = used_by_str; |
| blocks[block].used_by_block = used_by_block; |
| } |
| assert(blocks[block].used_by_str == used_by_str); |
| assert(blocks[block].used_by_block == used_by_block); |
| } |
| } |
| |
| static bool block_set_replace_used_by(data_block_t block, |
| const char* old_used_by_str, |
| const char* new_used_by_str, |
| data_block_t new_used_by_block, |
| bool checkpoint) { |
| if (!blocks[block].used_by_str || |
| strcmp(blocks[block].used_by_str, old_used_by_str) != 0) { |
| return false; |
| } |
| |
| block_set_used_by_etc(block, new_used_by_str, new_used_by_block, checkpoint, |
| true); |
| return true; |
| } |
| |
| static void block_set_used_by(data_block_t block, |
| const char* used_by_str, |
| data_block_t used_by_block) { |
| block_set_used_by_etc(block, used_by_str, used_by_block, false, false); |
| } |
| |
| static void mark_block_tree_in_use(struct transaction* tr, |
| struct block_tree* block_tree, |
| bool mark_data_used, |
| const char* used_by_str, |
| data_block_t used_by_block, |
| bool checkpoint) { |
| struct block_tree_path path; |
| unsigned int i; |
| |
| block_tree_walk(tr, block_tree, 0, true, &path); |
| if (path.count) { |
| /* mark root in use in case it is empty */ |
| block_set_used_by_etc(block_mac_to_block(tr, &path.entry[0].block_mac), |
| used_by_str, used_by_block, checkpoint, false); |
| } |
| while (block_tree_path_get_key(&path)) { |
| for (i = 0; i < path.count; i++) { |
| block_set_used_by_etc( |
| block_mac_to_block(tr, &path.entry[i].block_mac), |
| used_by_str, used_by_block, checkpoint, false); |
| } |
| if (mark_data_used) { |
| block_set_used_by_etc(block_tree_path_get_data(&path), used_by_str, |
| used_by_block, checkpoint, false); |
| } |
| block_tree_path_next(&path); |
| } |
| } |
| |
| static void mark_files_in_use(struct transaction* tr) { |
| void file_block_map_init(struct transaction * tr, |
| struct block_map * block_map, |
| const struct block_mac* file); |
| |
| struct block_tree_path path; |
| struct block_map block_map; |
| |
| block_tree_walk(tr, &tr->fs->files, 0, true, &path); |
| while (block_tree_path_get_key(&path)) { |
| struct block_mac block_mac = block_tree_path_get_data_block_mac(&path); |
| file_block_map_init(tr, &block_map, &block_mac); |
| mark_block_tree_in_use(tr, &block_map.tree, true, "file", |
| block_mac_to_block(tr, &block_mac), false); |
| block_tree_path_next(&path); |
| } |
| } |
| |
| static void check_fs_prepare(struct transaction* tr) { |
| data_block_t block; |
| struct block_tree checkpoint_files = |
| BLOCK_TREE_INITIAL_VALUE(checkpoint_files); |
| size_t block_mac_size = tr->fs->block_num_size + tr->fs->mac_size; |
| block_tree_init(&checkpoint_files, tr->fs->dev->block_size, |
| tr->fs->block_num_size, block_mac_size, block_mac_size); |
| |
| block_clear_used_by(); |
| |
| for (block = tr->fs->dev->block_count; block < countof(blocks); block++) { |
| block_set_used_by(block, "out-of-range", 0); |
| } |
| |
| block_set_used_by(tr->fs->super_block[0], "superblock", 0); |
| block_set_used_by(tr->fs->super_block[1], "superblock", 1); |
| |
| block = 1; |
| while (true) { |
| block = block_set_find_next_block(tr, &tr->fs->free, block, true); |
| if (!block) { |
| break; |
| } |
| block_set_used_by(block, "free", 0); |
| block++; |
| } |
| |
| mark_block_tree_in_use(tr, &tr->fs->free.block_tree, false, |
| "free_tree_node", 0, false); |
| |
| mark_block_tree_in_use(tr, &tr->fs->files, true, "files", 0, false); |
| mark_files_in_use(tr); |
| |
| if (block_mac_valid(tr, &tr->fs->checkpoint)) { |
| assert(checkpoint_read(tr, &tr->fs->checkpoint, &checkpoint_files, |
| NULL)); |
| block_set_used_by_etc(block_mac_to_block(tr, &tr->fs->checkpoint), |
| "checkpoint", 0, true, false); |
| mark_block_tree_in_use(tr, &checkpoint_files, true, "checkpoint_files", |
| 0, true); |
| mark_block_tree_in_use(tr, &tr->fs->checkpoint_free.block_tree, false, |
| "checkpoint_free", 0, true); |
| } |
| } |
| |
| static bool check_fs_finish(struct transaction* tr) { |
| bool valid = true; |
| data_block_t block; |
| |
| for (block = 0; block < countof(blocks); block++) { |
| if (!blocks[block].used_by_str && |
| !blocks[block].checkpoint_used_by_str) { |
| printf("block %" PRIu64 ", lost\n", block); |
| valid = false; |
| } |
| } |
| |
| if (!valid) { |
| printf("free:\n"); |
| block_set_print(tr, &tr->fs->free); |
| files_print(tr); |
| printf("checkpoint free:\n"); |
| block_set_print(tr, &tr->fs->checkpoint_free); |
| } |
| |
| return valid; |
| } |
| |
| static size_t get_fs_checkpoint_count(struct transaction* tr) { |
| data_block_t block; |
| size_t checkpoint_count = 0; |
| check_fs_prepare(tr); |
| for (block = 0; block < countof(blocks); block++) { |
| if (blocks[block].checkpoint_used_by_str && |
| strncmp("checkpoint", blocks[block].checkpoint_used_by_str, |
| strlen("checkpoint")) == 0) { |
| checkpoint_count++; |
| } |
| } |
| assert(check_fs_finish(tr)); |
| return checkpoint_count; |
| } |
| |
| static bool check_fs_allocated(struct transaction* tr, |
| data_block_t* allocated, |
| size_t allocated_count) { |
| size_t i; |
| |
| check_fs_prepare(tr); |
| |
| for (i = 0; i < allocated_count; i++) { |
| block_set_used_by(allocated[i], "allocated", 0); |
| } |
| |
| return check_fs_finish(tr); |
| } |
| |
| static bool check_fs(struct transaction* tr) { |
| return check_fs_allocated(tr, NULL, 0); |
| } |
| #endif |
| |
| static void empty_test(struct transaction* tr) { |
| struct block_range range; |
| |
| range.start = 4; |
| range.end = tr->fs->dev->block_count; |
| assert(block_set_range_in_set(tr, &tr->fs->free, range)); |
| |
| if (print_test_verbose) { |
| printf("%s: initial free state:\n", __func__); |
| block_set_print(tr, &tr->fs->free); |
| } |
| } |
| |
| typedef uint16_t (*keyfunc_t)(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex); |
| static uint16_t inc_inc_key(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex) { |
| return rindex ?: index; |
| } |
| |
| static uint16_t inc_dec_key(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex) { |
| return index; |
| } |
| |
| static uint16_t dec_inc_key(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex) { |
| return maxindex + 1 - index; |
| } |
| |
| static uint16_t dec_dec_key(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex) { |
| return maxindex + 1 - (rindex ?: index); |
| } |
| |
| static uint16_t same_key(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex) { |
| return 1; |
| } |
| |
| static uint16_t rand_key(unsigned int index, |
| unsigned int rindex, |
| unsigned int maxindex) { |
| uint16_t key; |
| |
| RAND_bytes((uint8_t*)&key, sizeof(key)); |
| |
| return key ?: 1; /* 0 key is not currently supported */ |
| } |
| |
| keyfunc_t keyfuncs[] = { |
| inc_inc_key, inc_dec_key, dec_inc_key, dec_dec_key, same_key, rand_key, |
| }; |
| |
| static void block_tree_test_etc(struct transaction* tr, |
| unsigned int order, |
| unsigned int count, |
| unsigned int commit_interval, |
| keyfunc_t keyfunc) { |
| unsigned int i; |
| unsigned int ri; |
| uint16_t key; |
| uint16_t tmpkey; |
| unsigned int commit_count = 0; |
| struct block_tree tree = BLOCK_TREE_INITIAL_VALUE(tree); |
| struct block_tree_path path; |
| const size_t key_size = sizeof(key); |
| const size_t header_size = sizeof(struct iv) + 8; |
| const size_t child_size = sizeof(struct block_mac); |
| size_t block_size = |
| header_size + key_size * (order - 1) + child_size * order; |
| |
| if (block_size > tr->fs->dev->block_size) { |
| printf("block tree order %d does not fit in block. block size %zd > %zd, skip test\n", |
| order, block_size, tr->fs->dev->block_size); |
| return; |
| } |
| |
| block_tree_init(&tree, block_size, key_size, child_size, child_size); |
| if (commit_interval) { |
| tree.copy_on_write = true; |
| tree.allow_copy_on_write = true; |
| } |
| |
| assert(tree.key_count[0] == order - 1); |
| assert(tree.key_count[1] == order - 1); |
| |
| for (i = 1; i <= count; key++, i++) { |
| key = keyfunc(i, 0, count); |
| if (print_block_tree_test_verbose) { |
| printf("block tree order %d, insert %d:\n", order, key); |
| } |
| block_tree_insert(tr, &tree, key, i); |
| if (tr->failed) { |
| return; |
| } |
| if (commit_interval && ++commit_count == commit_interval) { |
| commit_count = 0; |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| } |
| if (print_block_tree_test_verbose) { |
| printf("block tree order %d after %d inserts, last %d:\n", order, i, |
| key); |
| block_tree_print(tr, &tree); |
| } |
| } |
| for (ri = 1, i--; i >= 1; i--, ri++) { |
| tmpkey = keyfunc(i, ri, count); |
| assert(tmpkey); |
| block_tree_walk(tr, &tree, tmpkey, false, &path); |
| key = block_tree_path_get_key(&path); |
| if (!key) { |
| key = path.entry[path.count - 1].prev_key; |
| assert(key); |
| } |
| if (key != tmpkey) { |
| block_tree_walk(tr, &tree, key, false, &path); |
| } |
| assert(key = block_tree_path_get_key(&path)); |
| assert(block_tree_path_get_data(&path)); |
| if (print_block_tree_test_verbose) { |
| printf("block tree order %d, remove %d (%d):\n", order, key, |
| tmpkey); |
| } |
| block_tree_remove(tr, &tree, key, block_tree_path_get_data(&path)); |
| if (tr->failed) { |
| return; |
| } |
| if (commit_interval && ++commit_count == commit_interval) { |
| commit_count = 0; |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| } |
| if (print_block_tree_test_verbose) { |
| printf("block tree order %d removed %d:\n", order, key); |
| block_tree_print(tr, &tree); |
| } |
| } |
| |
| if (commit_interval) { |
| block_discard_dirty_by_block(tr->fs->dev, |
| block_mac_to_block(tr, &tree.root)); |
| block_free(tr, block_mac_to_block(tr, &tree.root)); |
| } |
| } |
| |
| static void block_tree_keyfuncs_test(struct transaction* tr, |
| unsigned int order, |
| unsigned int count) { |
| unsigned int commit_interval; |
| unsigned int i; |
| |
| for (commit_interval = 0; commit_interval < 2; commit_interval++) { |
| for (i = 0; i < countof(keyfuncs); i++) { |
| block_tree_test_etc(tr, order, count, commit_interval, keyfuncs[i]); |
| } |
| } |
| } |
| |
| static void block_tree_test(struct transaction* tr) { |
| unsigned int order; |
| |
| block_tree_keyfuncs_test(tr, 6, 5); /* test leaf node only */ |
| block_tree_keyfuncs_test(tr, 6, 10); |
| |
| for (order = 3; order <= 5; order++) { |
| block_tree_keyfuncs_test(tr, order, order - 1); |
| block_tree_keyfuncs_test(tr, order, order); |
| block_tree_keyfuncs_test(tr, order, order * 2); |
| block_tree_keyfuncs_test(tr, order, order * order); |
| block_tree_keyfuncs_test(tr, order, order * order * order); |
| } |
| } |
| |
| static void block_set_test(struct transaction* tr) { |
| struct block_set sets[3]; |
| unsigned int si, i; |
| |
| for (si = 0; si < countof(sets); si++) { |
| block_set_init(tr->fs, &sets[si]); |
| } |
| |
| for (i = 0; i < 10; i++) { |
| for (si = 0; si < countof(sets); si++) { |
| block_set_add_block(tr, &sets[si], 2 + i * 3 + si); |
| } |
| } |
| for (si = 0; si < countof(sets); si++) { |
| assert(!block_set_overlap(tr, &sets[si], |
| &sets[(si + 1) % countof(sets)])); |
| } |
| for (si = 1; si < countof(sets); si++) { |
| block_set_add_block(tr, &sets[0], 2 + 5 * 3 + si); |
| } |
| for (si = 1; si < countof(sets); si++) { |
| assert(block_set_overlap(tr, &sets[si], &sets[0])); |
| assert(block_set_overlap(tr, &sets[0], &sets[si])); |
| assert(si < 2 || !block_set_overlap(tr, &sets[si], &sets[si - 1])); |
| } |
| } |
| |
| static void block_tree_allocate_all_test(struct transaction* tr) { |
| unsigned int i; |
| |
| for (i = 0; i < countof(keyfuncs); i++) { |
| assert(!tr->failed); |
| block_tree_test_etc(tr, 3, UINT_MAX, 0, keyfuncs[i]); |
| assert(tr->failed); |
| transaction_complete(tr); |
| transaction_activate(tr); |
| } |
| } |
| static void block_map_test(struct transaction* tr) { |
| unsigned int i; |
| struct block_mac block_mac = BLOCK_MAC_INITIAL_VALUE(block_mac); |
| struct block_map block_map = BLOCK_MAP_INITIAL_VALUE(block_map); |
| |
| block_map_init(tr, &block_map, &block_mac, 128); |
| |
| for (i = 1; i <= 100; i++) { |
| block_mac_set_block(tr, &block_mac, block_allocate(tr)); |
| block_map_set(tr, &block_map, i, &block_mac); |
| } |
| for (; i >= 2; i /= 2) { |
| block_map_truncate(tr, &block_map, i); |
| assert(!block_map_get(tr, &block_map, i, &block_mac)); |
| assert(block_map_get(tr, &block_map, i - 1, &block_mac)); |
| } |
| block_map_free(tr, &block_map); |
| } |
| |
| static void free_frag_etc_test(struct transaction* tr, |
| int start, |
| int end, |
| int inc) { |
| int i; |
| |
| for (i = start; i < end; i += inc) { |
| block_free(tr, i); |
| assert(!tr->failed); |
| } |
| } |
| |
| static void allocate_2_transactions_test_etc(struct transaction* tr, |
| data_block_t blocks1[], |
| size_t blocks1_count, |
| data_block_t blocks2[], |
| size_t blocks2_count) { |
| unsigned int i; |
| struct transaction tr1; |
| struct transaction tr2; |
| size_t blocks_max_count = MAX(blocks1_count, blocks2_count); |
| |
| transaction_init(&tr1, tr->fs, true); |
| transaction_init(&tr2, tr->fs, true); |
| |
| for (i = 0; i < blocks_max_count; i++) { |
| if (i < blocks1_count) { |
| blocks1[i] = block_allocate(&tr1); |
| } |
| if (i < blocks2_count) { |
| blocks2[i] = block_allocate(&tr2); |
| } |
| } |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| |
| transaction_complete(&tr1); |
| |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| |
| for (i = 0; i < blocks1_count; i++) { |
| assert(!block_set_block_in_set(tr, &tr->fs->free, blocks1[i])); |
| } |
| for (i = 0; i < blocks2_count; i++) { |
| assert(block_set_block_in_set(tr, &tr->fs->free, blocks2[i])); |
| } |
| |
| transaction_complete(&tr2); |
| |
| for (i = 0; i < blocks1_count; i++) { |
| assert(!block_set_block_in_set(tr, &tr->fs->free, blocks1[i])); |
| } |
| for (i = 0; i < blocks2_count; i++) { |
| assert(!block_set_block_in_set(tr, &tr->fs->free, blocks2[i])); |
| } |
| |
| assert(!block_cache_debug_get_ref_block_count()); |
| #if FULL_ASSERT |
| check_fs_prepare(tr); |
| for (i = 0; i < blocks1_count; i++) { |
| block_set_used_by(blocks1[i], "allocated", 0); |
| } |
| for (i = 0; i < blocks2_count; i++) { |
| block_set_used_by(blocks2[i], "allocated2", 0); |
| } |
| assert(check_fs_finish(tr)); |
| #endif |
| |
| transaction_free(&tr1); |
| transaction_free(&tr2); |
| } |
| |
| static void free_test_etc(struct transaction* tr, |
| data_block_t blocks1[], |
| size_t blocks1_count, |
| data_block_t blocks2[], |
| size_t blocks2_count) { |
| unsigned int i; |
| struct transaction tr1; |
| struct transaction tr2; |
| size_t blocks_max_count = MAX(blocks1_count, blocks2_count); |
| |
| transaction_init(&tr1, tr->fs, true); |
| transaction_init(&tr2, tr->fs, true); |
| |
| for (i = 0; i < blocks_max_count; i++) { |
| if (i < blocks1_count) { |
| block_free(&tr1, blocks1[i]); |
| if (print_test_verbose) { |
| printf("tr1.freed after free %" PRIu64 ":\n", blocks1[i]); |
| block_set_print(&tr1, &tr1.freed); |
| } |
| } |
| if (i < blocks2_count) { |
| block_free(&tr2, blocks1[i]); |
| if (print_test_verbose) { |
| printf("tr2.freed after free %" PRIu64 ":\n", blocks2[i]); |
| block_set_print(&tr2, &tr2.freed); |
| } |
| } |
| } |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| |
| transaction_complete(&tr1); |
| |
| assert(!tr1.failed); |
| |
| for (i = 0; i < blocks1_count; i++) { |
| assert(block_set_block_in_set(tr, &tr->fs->free, blocks1[i])); |
| } |
| |
| if (blocks1 == blocks2) { |
| /* free conflict test */ |
| assert(tr2.failed); |
| } |
| transaction_complete(&tr2); |
| |
| if (blocks1 != blocks2) { |
| assert(!tr2.failed); |
| for (i = 0; i < blocks2_count; i++) { |
| assert(block_set_block_in_set(tr, &tr->fs->free, blocks2[i])); |
| } |
| } |
| |
| assert(!block_cache_debug_get_ref_block_count()); |
| |
| transaction_free(&tr1); |
| transaction_free(&tr2); |
| } |
| |
| /* clang-format off */ |
| enum { |
| test_free_start = BLOCK_SIZE > 112 ? 20 : 200, |
| test_free_split = test_free_start + (BLOCK_SIZE > 112 ? 60 : 20), |
| test_free_end = BLOCK_SIZE > 96 ? BLOCK_COUNT |
| : BLOCK_SIZE > 80 ? BLOCK_COUNT - 8 |
| : BLOCK_COUNT - 16, |
| test_free_increment = BLOCK_SIZE > 64 ? 2 : 20, |
| |
| allocated_size = BLOCK_SIZE > 128 ? 40 : 10, |
| allocated2_size = BLOCK_SIZE > 64 ? allocated_size : 4, |
| }; |
| /* clang-format on */ |
| static data_block_t allocated[allocated_size]; |
| static data_block_t allocated2[allocated2_size]; |
| |
| static void allocate_frag_test(struct transaction* tr) { |
| int i; |
| struct block_range range; |
| |
| range.start = test_free_start; |
| range.end = test_free_end; |
| block_set_add_range(tr, &tr->allocated, range); |
| assert(!block_cache_debug_get_ref_block_count()); |
| |
| for (i = test_free_start; i + 1 < test_free_end; i += test_free_increment) { |
| if (print_test_verbose) { |
| printf("%s: remove block %d\n", __func__, i); |
| } |
| range.start = i + 1; |
| range.end = i + test_free_increment; |
| if (range.end > test_free_end) { |
| range.end = test_free_end; |
| } |
| block_set_remove_range(tr, &tr->allocated, range); |
| assert(!block_cache_debug_get_ref_block_count()); |
| assert(!tr->failed); |
| } |
| |
| if (print_test_verbose) { |
| printf("%s: tr.tmp_allocated:\n", __func__); |
| block_set_print(tr, &tr->tmp_allocated); |
| printf("%s: tr.allocated:\n", __func__); |
| block_set_print(tr, &tr->allocated); |
| } |
| |
| assert(!tr->failed); |
| assert(!block_cache_debug_get_ref_block_count()); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| if (print_test_verbose) { |
| printf("%s: free state after transaction complete:\n", __func__); |
| block_set_print(tr, &tr->fs->free); |
| } |
| assert(!block_cache_debug_get_ref_block_count()); |
| #if FULL_ASSERT |
| check_fs_prepare(tr); |
| for (i = test_free_start; i < test_free_end; i += test_free_increment) { |
| block_set_used_by(i, "test_free_fragmentation", 0); |
| } |
| assert(check_fs_finish(tr)); |
| #endif |
| } |
| |
| static void allocate_free_same_test(struct transaction* tr) { |
| unsigned int i; |
| printf("%s: start allocate then free same test\n", __func__); |
| for (i = 0; i < countof(allocated); i++) { |
| allocated[i] = block_allocate(tr); |
| assert(!tr->failed); |
| } |
| if (print_test_verbose) { |
| printf("%s: tr.tmp_allocated:\n", __func__); |
| block_set_print(tr, &tr->tmp_allocated); |
| printf("%s: tr.allocated:\n", __func__); |
| block_set_print(tr, &tr->allocated); |
| } |
| |
| for (i = 0; i < countof(allocated); i++) { |
| block_free(tr, allocated[i]); |
| } |
| assert(!tr->failed); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| assert(block_set_check(tr, &tr->fs->free)); |
| assert(!block_cache_debug_get_ref_block_count()); |
| #if FULL_ASSERT |
| check_fs_prepare(tr); |
| for (i = test_free_start; i < test_free_end; i += test_free_increment) { |
| block_set_used_by(i, "test_free_fragmentation", 0); |
| } |
| assert(check_fs_finish(tr)); |
| #endif |
| printf("%s: start allocate then free same test, done\n", __func__); |
| } |
| |
| static void allocate_free_other_test(struct transaction* tr) { |
| unsigned int i; |
| |
| printf("%s: start allocate then free some other test\n", __func__); |
| for (i = 0; i < countof(allocated); i++) { |
| allocated[i] = block_allocate(tr); |
| if (print_test_verbose) { |
| printf("tr.tmp_allocated after allocate %d, %" PRIu64 ":\n", i, |
| allocated[i]); |
| block_set_print(tr, &tr->tmp_allocated); |
| printf("tr.allocated after allocate %d, %" PRIu64 ":\n", i, |
| allocated[i]); |
| block_set_print(tr, &tr->allocated); |
| } |
| assert(!tr->failed); |
| } |
| for (i = test_free_start; i < test_free_split; i += test_free_increment) { |
| block_free(tr, i); |
| if (print_test_verbose) { |
| printf("tr.freed after free %d:\n", i); |
| block_set_print(tr, &tr->freed); |
| } |
| assert(!tr->failed); |
| } |
| if (print_test_verbose) { |
| printf("fs.super->free:\n"); |
| block_set_print(tr, &tr->fs->free); |
| } |
| assert(!tr->failed); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| assert(block_set_check(tr, &tr->fs->free)); |
| |
| for (i = 0; i < countof(allocated); i++) { |
| assert(!block_set_block_in_set(tr, &tr->fs->free, allocated[i])); |
| } |
| for (i = test_free_start; i < test_free_split; i += test_free_increment) { |
| assert(block_set_block_in_set(tr, &tr->fs->free, i)); |
| } |
| assert(!block_cache_debug_get_ref_block_count()); |
| #if FULL_ASSERT |
| check_fs_prepare(tr); |
| for (i = test_free_split; i < test_free_end; i += test_free_increment) { |
| block_set_used_by(i, "test_free_fragmentation", 0); |
| } |
| for (i = 0; i < countof(allocated); i++) { |
| block_set_used_by(allocated[i], "allocated", 0); |
| } |
| assert(check_fs_finish(tr)); |
| #endif |
| printf("%s: start allocate then free some other test, done\n", __func__); |
| } |
| |
| static void free_frag_rem_test(struct transaction* tr) { |
| int i; |
| printf("%s: start free rem test\n", __func__); |
| free_frag_etc_test(tr, test_free_split, test_free_end, test_free_increment); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| assert(block_set_check(tr, &tr->fs->free)); |
| for (i = test_free_split; i < test_free_end; i += test_free_increment) { |
| assert(block_set_block_in_set(tr, &tr->fs->free, i)); |
| } |
| assert(!block_cache_debug_get_ref_block_count()); |
| full_assert(check_fs_allocated(tr, allocated, countof(allocated))); |
| printf("%s: start free rem test, done\n", __func__); |
| } |
| |
| static void free_test(struct transaction* tr) { |
| unsigned int i; |
| |
| free_test_etc(tr, allocated, countof(allocated), NULL, 0); |
| |
| i = 0; |
| do { |
| // TODO: use this version, currently does not work since ranges aer not |
| // merged accross nodes |
| // i = block_set_find_next_block(&fs.free, i, false); |
| while (block_set_find_next_block(tr, &tr->fs->free, i, true) == i) { |
| i++; |
| } |
| int free = block_set_find_next_block(tr, &tr->fs->free, i, true); |
| printf("not free: [%d-%d]\n", i, free - 1); |
| i = free; |
| } while (i); |
| } |
| |
| static void allocate_2_transactions_test(struct transaction* tr) { |
| printf("%s: start allocate 2 transactions test\n", __func__); |
| allocate_2_transactions_test_etc(tr, allocated, countof(allocated), |
| allocated2, countof(allocated2)); |
| printf("%s: allocate 2 transactions test done\n", __func__); |
| } |
| |
| static void free_2_transactions_same_test(struct transaction* tr) { |
| printf("%s: start free 2 transactions same test\n", __func__); |
| free_test_etc(tr, allocated, countof(allocated), allocated, |
| countof(allocated)); |
| full_assert(check_fs_allocated(tr, allocated2, countof(allocated2))); |
| printf("%s: free 2 transactions same test done\n", __func__); |
| } |
| |
| static void free_2_transactions_same_test_2(struct transaction* tr) { |
| printf("%s: start free 2 transactions same test 2\n", __func__); |
| free_test_etc(tr, allocated2, countof(allocated2), allocated2, |
| countof(allocated2)); |
| full_assert(check_fs(tr)); |
| printf("%s: free 2 transactions same test 2 done\n", __func__); |
| } |
| |
| static void allocate_all_test(struct transaction* tr) { |
| while (block_allocate(tr)) { |
| assert(!tr->failed); |
| } |
| assert(tr->failed); |
| transaction_complete(tr); |
| transaction_activate(tr); |
| } |
| |
| static void super_block_write_failure_test(struct transaction* tr) { |
| data_block_t block1 = block_allocate(tr); |
| /* trigger a superblock write failure */ |
| block_test_fail_write_blocks = 2; |
| transaction_complete(tr); |
| block_test_fail_write_blocks = 0; |
| assert(tr->failed); |
| transaction_activate(tr); |
| assert(block_allocate(tr) == block1); |
| transaction_complete(tr); |
| transaction_activate(tr); |
| block_free(tr, block1); |
| } |
| |
| /* Test that block_put_dirty_discard actually drops the reference */ |
| static void block_put_dirty_discard_test(struct transaction* tr) { |
| struct obj_ref super_ref = OBJ_REF_INITIAL_VALUE(super_ref); |
| struct fs* fs = tr->fs; |
| const void* super_ro; |
| uint32_t* super_rw; |
| data_block_t block; |
| |
| block = tr->fs->super_block[fs->super_block_version & 1]; |
| super_ro = block_get_super(fs, block, &super_ref); |
| assert(super_ro); |
| super_rw = block_dirty(tr, super_ro, false); |
| assert(super_rw); |
| /* |
| * As part of dropping the dirty block we need to clear it from the block |
| * cache with block_cache_entry_discard_dirty, which requires that the block |
| * not have any active references. Verify that block_put_dirty_discard drops |
| * super_ref before trying to drop the block itself. |
| */ |
| block_put_dirty_discard(super_rw, &super_ref); |
| } |
| |
| static void open_test_file_etc(struct transaction* tr, |
| struct storage_file_handle* file, |
| const char* path, |
| enum file_create_mode create, |
| enum file_op_result expected_result) { |
| enum file_op_result result; |
| result = file_open(tr, path, file, create, allow_repaired); |
| if (print_test_verbose) { |
| printf("%s: lookup file %s, create %d, got %" PRIu64 ":\n", __func__, |
| path, create, block_mac_to_block(tr, &file->block_mac)); |
| } |
| |
| assert(result == expected_result); |
| assert(result != FILE_OP_SUCCESS || block_mac_valid(tr, &file->block_mac)); |
| } |
| |
| static void open_test_file(struct transaction* tr, |
| struct storage_file_handle* file, |
| const char* path, |
| enum file_create_mode create) { |
| open_test_file_etc(tr, file, path, create, FILE_OP_SUCCESS); |
| } |
| |
| static void file_allocate_all_test(struct transaction* master_tr, |
| unsigned int tr_count, |
| int success_count, |
| int step_size, |
| const char* path, |
| enum file_create_mode create) { |
| unsigned int i; |
| unsigned int j; |
| unsigned int done; |
| unsigned int count; |
| struct storage_file_handle file[tr_count]; |
| data_block_t file_size[tr_count]; |
| struct transaction tr[tr_count]; |
| int written_count[tr_count]; |
| size_t file_block_size = master_tr->fs->dev->block_size - sizeof(struct iv); |
| void* block_data_rw; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| |
| for (i = 0; i < tr_count; i++) { |
| transaction_init(&tr[i], master_tr->fs, true); |
| } |
| |
| for (count = INT_MAX, done = 0; count > 0; count -= step_size) { |
| for (i = 0; i < tr_count; i++) { |
| open_test_file(&tr[i], &file[i], path, create); |
| file_size[i] = file[i].size; |
| written_count[i] = 0; |
| } |
| |
| for (j = 0, done = 0; done != tr_count && j < count; j++) { |
| for (i = 0, done = 0; i < tr_count; i++) { |
| if (tr[i].failed) { |
| done++; |
| assert(j); |
| continue; |
| } |
| block_data_rw = |
| file_get_block_write(&tr[i], &file[i], j, true, &ref); |
| if (!block_data_rw) { |
| done++; |
| continue; |
| } |
| assert(!tr[i].failed); |
| file_block_put_dirty(&tr[i], &file[i], j, block_data_rw, &ref); |
| written_count[i] = j + 1; |
| } |
| } |
| for (i = 0, done = 0; i < tr_count; i++) { |
| file_set_size(&tr[i], &file[i], written_count[i] * file_block_size); |
| if (count == INT_MAX) { |
| assert(tr[i].failed); |
| } |
| transaction_complete(&tr[i]); |
| if (!tr[i].failed) { |
| assert(file[i].size == written_count[i] * file_block_size); |
| done++; |
| } else if (!done) { |
| assert(file_size[i] == file[i].size); |
| } |
| file_close(&file[i]); |
| transaction_activate(&tr[i]); |
| } |
| if (count == INT_MAX) { |
| count = j; |
| } |
| if (success_count && done) { |
| success_count--; |
| } |
| if (!success_count) { |
| break; |
| } |
| if (done) { |
| file_delete(&tr[0], path, false); |
| transaction_complete(&tr[0]); |
| assert(!tr[0].failed); |
| transaction_activate(&tr[0]); |
| } |
| } |
| assert(!success_count); |
| for (i = 0; i < tr_count; i++) { |
| transaction_complete(&tr[i]); |
| transaction_free(&tr[i]); |
| } |
| } |
| |
| static void file_create_all_test(struct transaction* tr) { |
| int i; |
| char path[4 + 8 + 1]; |
| struct storage_file_handle file; |
| |
| enum file_op_result result; |
| for (i = 0;; i++) { |
| snprintf(path, sizeof(path), "test%08x", i); |
| result = file_open(tr, path, &file, FILE_OPEN_CREATE_EXCLUSIVE, false); |
| if (result != FILE_OP_SUCCESS) { |
| break; |
| } |
| file_close(&file); |
| assert(!tr->failed); |
| } |
| |
| assert(tr->failed); |
| transaction_complete(tr); |
| transaction_activate(tr); |
| } |
| |
| /* run tests on already open file */ |
| static void file_test_open(struct transaction* tr, |
| struct storage_file_handle* file, |
| int allocate, |
| int read, |
| int free, |
| int id) { |
| int i; |
| int* block_data_rw; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| const int* block_data_ro; |
| size_t file_block_size = tr->fs->dev->block_size - sizeof(struct iv); |
| |
| if (allocate) { |
| for (i = 0; i < allocate; i++) { |
| block_data_rw = file_get_block_write(tr, file, i, true, &ref); |
| if (print_test_verbose) { |
| printf("%s: allocate file block %d, %" PRIu64 ":\n", __func__, |
| i, data_to_block_num(block_data_rw)); |
| } |
| assert(block_data_rw); |
| /* TODO: store iv in file block map */ |
| block_data_rw = (void*)block_data_rw + sizeof(struct iv); |
| // block_data_rw = block_get_cleared(block)+ sizeof(struct iv); |
| block_data_rw[0] = i; |
| block_data_rw[1] = ~i; |
| block_data_rw[2] = id; |
| block_data_rw[3] = ~id; |
| file_block_put_dirty(tr, file, i, |
| (void*)block_data_rw - sizeof(struct iv), |
| &ref); |
| } |
| if (file->size < i * file_block_size) { |
| file_set_size(tr, file, i * file_block_size); |
| } |
| assert(file->size >= i * file_block_size); |
| if (print_test_verbose) { |
| printf("%s: allocated %d file blocks\n", __func__, i); |
| file_print(tr, file); |
| } |
| } |
| |
| if (read) { |
| for (i = 0;; i++) { |
| block_data_ro = file_get_block(tr, file, i, &ref); |
| if (!block_data_ro) { |
| break; |
| } |
| if (print_test_verbose) { |
| printf("%s: found file block %d, %" PRIu64 ":\n", __func__, i, |
| data_to_block_num(block_data_ro)); |
| } |
| block_data_ro = (void*)block_data_ro + sizeof(struct iv); |
| assert(block_data_ro[0] == i); |
| assert(block_data_ro[1] == ~i); |
| assert(block_data_ro[2] == id); |
| assert(block_data_ro[3] == ~id); |
| file_block_put((void*)block_data_ro - sizeof(struct iv), &ref); |
| } |
| assert(i == read); |
| assert(file->size >= i * file_block_size); |
| } |
| |
| if (free) { |
| file_set_size(tr, file, 0); |
| for (i = 0; i < free; i++) { |
| block_data_ro = file_get_block(tr, file, i, &ref); |
| if (block_data_ro) { |
| file_block_put(block_data_ro, &ref); |
| printf("%s: file block %d, %" PRIu64 " not deleted\n", __func__, |
| i, data_to_block_num(block_data_ro)); |
| break; |
| } |
| } |
| if (print_test_verbose) { |
| printf("%s: deleted %d file blocks\n", __func__, i); |
| file_print(tr, file); |
| } |
| assert(i == free); |
| } |
| } |
| |
| static void file_test_commit(struct transaction* tr, bool commit) { |
| if (commit) { |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| } |
| } |
| |
| static void file_move_expect(struct transaction* tr, |
| const char* src_path, |
| enum file_create_mode src_create, |
| const char* dest_path, |
| enum file_create_mode dest_create, |
| enum file_op_result expected_result) { |
| struct storage_file_handle file; |
| enum file_op_result res; |
| |
| assert(!tr->failed); |
| |
| open_test_file(tr, &file, src_path, src_create); |
| |
| res = file_move(tr, &file, dest_path, dest_create, false); |
| assert(res == expected_result); |
| assert(!tr->failed); |
| file_close(&file); |
| } |
| |
| static void file_move_expect_success(struct transaction* tr, |
| const char* src_path, |
| enum file_create_mode src_create, |
| const char* dest_path, |
| enum file_create_mode dest_create) { |
| file_move_expect(tr, src_path, src_create, dest_path, dest_create, |
| FILE_OP_SUCCESS); |
| } |
| |
| static void file_test_etc(struct transaction* tr, |
| bool commit, |
| const char* path, |
| enum file_create_mode create, |
| const char* move_path, |
| enum file_create_mode move_create, |
| int allocate, |
| int read, |
| int free, |
| bool delete, |
| int id) { |
| enum file_op_result delete_res; |
| struct storage_file_handle file; |
| |
| open_test_file(tr, &file, path, create); |
| |
| file_test_commit(tr, commit); |
| |
| if (move_path) { |
| file_move(tr, &file, move_path, move_create, allow_repaired); |
| file_test_commit(tr, commit); |
| path = move_path; |
| } |
| file_test_open(tr, &file, allocate, read, free, id); |
| file_test_commit(tr, commit); |
| |
| if (delete) { |
| if (print_test_verbose) { |
| printf("%s: delete file %s, at %" PRIu64 ":\n", __func__, path, |
| block_mac_to_block(tr, &file.block_mac)); |
| } |
| delete_res = file_delete(tr, path, allow_repaired); |
| file_test_commit(tr, commit); |
| assert(delete_res == FILE_OP_SUCCESS); |
| } |
| |
| file_close(&file); |
| } |
| |
| static void file_test(struct transaction* tr, |
| const char* path, |
| enum file_create_mode create, |
| int allocate, |
| int read, |
| int free, |
| bool delete, |
| int id) { |
| file_test_etc(tr, false, path, create, NULL, FILE_OPEN_NO_CREATE, allocate, |
| read, free, delete, id); |
| } |
| |
| static void file_test_split_tr(struct transaction* tr, |
| const char* path, |
| enum file_create_mode create, |
| int open_count, |
| int allocate_file_index, |
| int allocate_index, |
| int allocate, |
| int read, |
| int free, |
| bool delete, |
| int id) { |
| enum file_op_result delete_res; |
| int i; |
| struct storage_file_handle file[open_count]; |
| |
| assert(allocate_file_index <= allocate_index); |
| assert(allocate_index < open_count); |
| |
| for (i = 0; i < open_count; i++) { |
| open_test_file(tr, &file[i], path, |
| (i == 0) ? create : FILE_OPEN_NO_CREATE); |
| assert(file[i].size == 0 || i > allocate_index); |
| if (i == allocate_index) { |
| file_test_open(tr, &file[allocate_file_index], allocate, 0, 0, id); |
| } |
| } |
| |
| if (create == FILE_OPEN_NO_CREATE) { |
| transaction_fail(tr); |
| transaction_activate(tr); |
| for (i = 0; i < open_count; i++) { |
| assert(file[i].size == 0); |
| } |
| |
| file_test_open(tr, &file[allocate_file_index], allocate, 0, 0, id); |
| } |
| |
| assert(!tr->failed); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| for (i = 0; i < open_count; i++) { |
| assert(file[i].size != 0); |
| } |
| |
| for (i = 1; i < open_count; i++) { |
| file_test_open(tr, &file[i], 0, read, 0, id); |
| assert(!tr->failed); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| } |
| |
| file_test_open(tr, &file[0], 0, read, free, id); |
| |
| if (delete) { |
| if (print_test_verbose) { |
| printf("%s: delete file %s, at %" PRIu64 ":\n", __func__, path, |
| block_mac_to_block(tr, &file[i].block_mac)); |
| } |
| delete_res = file_delete(tr, path, false); |
| assert(delete_res == FILE_OP_SUCCESS); |
| } |
| |
| for (i = 0; i < open_count; i++) { |
| file_close(&file[i]); |
| } |
| } |
| |
| static void file_read_after_delete_test(struct transaction* tr) { |
| const char* path = "test1s"; |
| struct storage_file_handle file; |
| struct storage_file_handle file2; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| const void* block_data_ro; |
| void* block_data_rw; |
| struct transaction tr2; |
| |
| transaction_init(&tr2, tr->fs, true); |
| |
| /* create test file */ |
| open_test_file(tr, &file, path, FILE_OPEN_CREATE_EXCLUSIVE); |
| block_data_rw = file_get_block_write(tr, &file, 0, false, &ref); |
| file_block_put_dirty(tr, &file, 0, block_data_rw, &ref); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| |
| /* open in second transaction */ |
| open_test_file(&tr2, &file2, path, FILE_OPEN_NO_CREATE); |
| |
| /* delete and try read in same transaction */ |
| transaction_activate(tr); |
| file_delete(tr, path, false); |
| block_data_ro = file_get_block(tr, &file, 0, &ref); |
| assert(!block_data_ro); |
| assert(tr->failed); |
| |
| /* read in second transaction */ |
| block_data_ro = file_get_block(&tr2, &file2, 0, &ref); |
| assert(block_data_ro); |
| file_block_put(block_data_ro, &ref); |
| |
| /* read file then delete file */ |
| transaction_activate(tr); |
| block_data_ro = file_get_block(tr, &file, 0, &ref); |
| assert(block_data_ro); |
| file_block_put(block_data_ro, &ref); |
| |
| file_delete(tr, path, false); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| |
| /* try to read in both transactions */ |
| transaction_activate(tr); |
| block_data_ro = file_get_block(tr, &file, 0, &ref); |
| assert(!block_data_ro); |
| assert(tr->failed); |
| |
| block_data_ro = file_get_block(&tr2, &file2, 0, &ref); |
| assert(!block_data_ro); |
| assert(tr2.failed); |
| |
| file_close(&file); |
| file_close(&file2); |
| |
| transaction_activate(tr); |
| transaction_free(&tr2); |
| } |
| |
| static const int file_test_block_count = BLOCK_SIZE > 64 ? 40 : 10; |
| static const int file_test_many_file_count = BLOCK_SIZE > 80 ? 40 : 10; |
| |
| static void file_create1_small_test(struct transaction* tr) { |
| file_test(tr, "test1s", FILE_OPEN_CREATE_EXCLUSIVE, 0, 0, 0, false, 1); |
| } |
| |
| static void file_write1_small_test(struct transaction* tr) { |
| file_test(tr, "test1s", FILE_OPEN_NO_CREATE, 1, 0, 0, false, 1); |
| } |
| |
| static void file_delete1_small_test(struct transaction* tr) { |
| file_test(tr, "test1s", FILE_OPEN_NO_CREATE, 0, 1, 1, true, 1); |
| } |
| |
| static void file_create_write_delete1_small_test(struct transaction* tr) { |
| file_test(tr, "test1s", FILE_OPEN_CREATE_EXCLUSIVE, 1, 1, 1, true, 1); |
| } |
| |
| static void file_splittr1_small_test(struct transaction* tr) { |
| file_test_split_tr(tr, "test1s", FILE_OPEN_NO_CREATE, 1, 0, 0, 1, 1, 0, |
| false, 1); |
| } |
| |
| static void file_splittr1o4_small_test(struct transaction* tr) { |
| #if 0 |
| /* |
| * Disabled test: Current file code does not allow having the same |
| * file open more than once per transaction. |
| */ |
| file_test_split_tr(tr, "test1s", FILE_OPEN_NO_CREATE, 4, 1, 2, 1, 1, 0, false, 1); |
| #else |
| struct storage_file_handle file[2]; |
| open_test_file_etc(tr, &file[0], "test1s", FILE_OPEN_NO_CREATE, |
| FILE_OP_SUCCESS); |
| open_test_file_etc(tr, &file[1], "test1s", FILE_OPEN_NO_CREATE, |
| FILE_OP_ERR_ALREADY_OPEN); |
| file_close(&file[0]); |
| file_splittr1_small_test(tr); |
| #endif |
| } |
| |
| static void file_splittr1c_small_test(struct transaction* tr) { |
| file_test_split_tr(tr, "test1s", FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, 1, 1, |
| 1, true, 1); |
| } |
| |
| static void file_splittr1o4c_small_test(struct transaction* tr) { |
| #if 0 |
| /* |
| * Disabled test: Current file code does not allow having the same |
| * file open more than once per transaction. |
| */ |
| file_test_split_tr(tr, "test1s", FILE_OPEN_CREATE_EXCLUSIVE, 4, 1, 2, 1, 1, 1, true, 1); |
| #endif |
| } |
| |
| static void file_splittr1o4cl_small_test(struct transaction* tr) { |
| #if 0 |
| /* |
| * Disabled test: Current file code does not allow having the same |
| * file open more than once per transaction. |
| */ |
| file_test_split_tr(tr, "test1s", FILE_OPEN_CREATE_EXCLUSIVE, 4, 2, 3, 1, 1, 1, true, 1); |
| #endif |
| } |
| |
| static void file_create1_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_CREATE_EXCLUSIVE, 0, 0, 0, false, 1); |
| } |
| |
| static void file_write1h_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, file_test_block_count / 2, 0, 0, |
| false, 1); |
| } |
| |
| static void file_write1_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, file_test_block_count, 0, 0, |
| false, 1); |
| } |
| |
| static void file_delete1_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, file_test_block_count, |
| file_test_block_count, true, 1); |
| } |
| |
| static void file_delete_create_write1_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, file_test_block_count, |
| file_test_block_count, true, 1); |
| file_test(tr, "test1", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, 0, |
| 0, false, 1); |
| } |
| |
| static void file_delete1_no_free_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, 0, 0, true, 1); |
| } |
| |
| static void file_move12_test(struct transaction* tr) { |
| file_test_etc(tr, false, "test1", FILE_OPEN_NO_CREATE, "test2", |
| FILE_OPEN_CREATE_EXCLUSIVE, 0, file_test_block_count, 0, |
| false, 1); |
| } |
| |
| static void file_move21_test(struct transaction* tr) { |
| file_test_etc(tr, true, "test2", FILE_OPEN_NO_CREATE, "test1", |
| FILE_OPEN_CREATE_EXCLUSIVE, 0, file_test_block_count, 0, |
| false, 1); |
| } |
| |
| static void file_create2_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, 0, |
| 0, false, 2); |
| file_test(tr, "test2", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, 0, |
| 0, false, 3); |
| } |
| |
| static void file_move_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, 0, |
| 0, false, 2); |
| file_test_etc(tr, false, "test1", FILE_OPEN_NO_CREATE, "test2", |
| FILE_OPEN_CREATE_EXCLUSIVE, 0, file_test_block_count, 0, |
| false, 2); |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, file_test_block_count, |
| file_test_block_count, true, 2); |
| |
| file_test(tr, "test1", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, 0, |
| 0, false, 2); |
| file_test(tr, "test2", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, 0, |
| 0, false, 3); |
| |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, file_test_block_count, false, |
| false, 2); |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, file_test_block_count, false, |
| false, 3); |
| |
| file_move_expect(tr, "test1", FILE_OPEN_NO_CREATE, "test3", |
| FILE_OPEN_NO_CREATE, FILE_OP_ERR_NOT_FOUND); |
| |
| file_move_expect(tr, "test1", FILE_OPEN_NO_CREATE, "test2", |
| FILE_OPEN_CREATE_EXCLUSIVE, FILE_OP_ERR_EXIST); |
| |
| file_move_expect(tr, "test1", FILE_OPEN_NO_CREATE, "test1", |
| FILE_OPEN_CREATE_EXCLUSIVE, FILE_OP_ERR_EXIST); |
| |
| file_move_expect_success(tr, "test1", FILE_OPEN_NO_CREATE, "test1", |
| FILE_OPEN_NO_CREATE); |
| |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| file_move_expect(tr, "test1", FILE_OPEN_NO_CREATE, "test3", |
| FILE_OPEN_NO_CREATE, FILE_OP_ERR_NOT_FOUND); |
| |
| file_move_expect(tr, "test1", FILE_OPEN_NO_CREATE, "test2", |
| FILE_OPEN_CREATE_EXCLUSIVE, FILE_OP_ERR_EXIST); |
| |
| file_move_expect(tr, "test1", FILE_OPEN_NO_CREATE, "test1", |
| FILE_OPEN_CREATE_EXCLUSIVE, FILE_OP_ERR_EXIST); |
| |
| file_move_expect_success(tr, "test1", FILE_OPEN_NO_CREATE, "test1", |
| FILE_OPEN_NO_CREATE); |
| |
| file_test_etc(tr, false, "test1", FILE_OPEN_NO_CREATE, "test2", |
| FILE_OPEN_NO_CREATE, 0, file_test_block_count, 0, false, 2); |
| |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, file_test_block_count, false, |
| false, 2); |
| |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, file_test_block_count, |
| file_test_block_count, true, 2); |
| } |
| |
| static void file_delete2_test(struct transaction* tr) { |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, file_test_block_count, false, |
| false, 2); |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, file_test_block_count, false, |
| false, 3); |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, file_test_block_count, |
| file_test_block_count, file_test_block_count, 2); |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, file_test_block_count, |
| file_test_block_count, file_test_block_count, 3); |
| } |
| |
| static void file_create2_read_after_commit_test(struct transaction* tr) { |
| int i; |
| struct storage_file_handle file[2]; |
| |
| open_test_file(tr, &file[0], "test1", FILE_OPEN_CREATE_EXCLUSIVE); |
| open_test_file(tr, &file[1], "test2", FILE_OPEN_CREATE_EXCLUSIVE); |
| |
| for (i = 0; i < 2; i++) { |
| file_test_open(tr, &file[i], file_test_block_count, 0, 0, 2 + i); |
| } |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| for (i = 0; i < 2; i++) { |
| file_test_open(tr, &file[i], 0, file_test_block_count, 0, 2 + i); |
| file_close(&file[i]); |
| } |
| } |
| |
| static void file_create3_conflict_test(struct transaction* tr) { |
| struct transaction tr1; |
| struct transaction tr2; |
| struct transaction tr3; |
| |
| transaction_init(&tr1, tr->fs, true); |
| transaction_init(&tr2, tr->fs, true); |
| transaction_init(&tr3, tr->fs, true); |
| |
| file_test(&tr1, "test1", FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 4); |
| file_test(&tr2, "test1", FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 5); |
| file_test(&tr3, "test2", FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 6); |
| |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| assert(!tr3.failed); |
| transaction_complete(&tr1); |
| transaction_complete(&tr2); |
| transaction_complete(&tr3); |
| assert(!tr1.failed); |
| assert(tr2.failed); |
| assert(!tr3.failed); |
| file_test(tr, "test1", FILE_OPEN_NO_CREATE, 0, 1, 1, true, 4); |
| file_test(tr, "test2", FILE_OPEN_NO_CREATE, 0, 1, 1, true, 6); |
| |
| transaction_free(&tr1); |
| transaction_free(&tr2); |
| transaction_free(&tr3); |
| } |
| |
| static void file_create_delete_2_transaction_test(struct transaction* tr) { |
| struct transaction tr1; |
| struct transaction tr2; |
| |
| transaction_init(&tr1, tr->fs, true); |
| transaction_init(&tr2, tr->fs, true); |
| |
| file_test(&tr1, "test1", FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 7); |
| file_test(&tr2, "test2", FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 8); |
| |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| transaction_complete(&tr1); |
| transaction_complete(&tr2); |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| |
| transaction_activate(&tr1); |
| transaction_activate(&tr2); |
| file_test(&tr1, "test1", FILE_OPEN_NO_CREATE, 0, 1, 1, true, 7); |
| file_test(&tr2, "test2", FILE_OPEN_NO_CREATE, 0, 1, 1, true, 8); |
| |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| transaction_complete(&tr1); |
| transaction_complete(&tr2); |
| assert(!tr1.failed); |
| assert(!tr2.failed); |
| |
| transaction_free(&tr1); |
| transaction_free(&tr2); |
| } |
| |
| static void file_create_many_test(struct transaction* tr) { |
| char path[10]; |
| int i; |
| for (i = 0; i < file_test_many_file_count; i++) { |
| snprintf(path, sizeof(path), "test%d", i); |
| file_test(tr, path, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 7 + i); |
| } |
| } |
| |
| static void file_delete_many_test(struct transaction* tr) { |
| char path[10]; |
| int i; |
| |
| files_print(tr); |
| |
| for (i = 0; i < file_test_many_file_count; i++) { |
| snprintf(path, sizeof(path), "test%d", i); |
| file_test(tr, path, false, 0, 1, 1, true, 7 + i); |
| } |
| } |
| |
| struct file_iterate_many_state { |
| struct file_iterate_state iter; |
| uint64_t found; |
| bool stop; |
| char last_path[10]; |
| }; |
| |
| static bool file_iterate_many_iter(struct file_iterate_state* iter, |
| struct transaction* tr, |
| const struct block_mac* block_mac, |
| bool added, |
| bool removed) { |
| struct file_iterate_many_state* miter = |
| containerof(iter, struct file_iterate_many_state, iter); |
| const struct file_info* file_info; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| int i; |
| int ret; |
| uint64_t mask; |
| |
| file_info = file_get_info(tr, block_mac, &ref); |
| |
| ret = sscanf(file_info->path, "test%d", &i); |
| |
| assert(strlen(file_info->path) < sizeof(miter->last_path)); |
| strcpy(miter->last_path, file_info->path); |
| |
| file_info_put(file_info, &ref); |
| |
| assert(ret == 1); |
| mask = (1ULL << i); |
| assert(!(miter->found & mask)); |
| miter->found |= mask; |
| |
| return miter->stop; |
| } |
| |
| static void file_iterate_many_test(struct transaction* tr) { |
| struct file_iterate_many_state state = { |
| .iter.file = file_iterate_many_iter, |
| .found = 0, |
| .stop = false, |
| }; |
| uint64_t last_found = 0; |
| enum file_op_result res; |
| |
| /* iterate over all files in one pass */ |
| res = file_iterate(tr, NULL, false, &state.iter, false); |
| assert(state.found = (1ull << file_test_many_file_count) - 1); |
| assert(res == FILE_OP_SUCCESS); |
| res = file_iterate(tr, NULL, true, &state.iter, false); |
| assert(res == FILE_OP_SUCCESS); |
| |
| /* lookup one file at a time */ |
| state.found = 0; |
| state.stop = true; |
| res = file_iterate(tr, NULL, false, &state.iter, false); |
| assert(res == FILE_OP_SUCCESS); |
| while (state.found != last_found) { |
| last_found = state.found; |
| res = file_iterate(tr, state.last_path, false, &state.iter, false); |
| assert(res == FILE_OP_SUCCESS); |
| } |
| assert(state.found = (1ull << file_test_many_file_count) - 1); |
| res = file_iterate(tr, NULL, true, &state.iter, false); |
| assert(res == FILE_OP_SUCCESS); |
| } |
| |
| static void file_allocate_all1_test(struct transaction* tr) { |
| file_allocate_all_test(tr, 1, 0, 1, "test1", FILE_OPEN_CREATE); |
| } |
| |
| static void file_allocate_all_2tr_1_test(struct transaction* tr) { |
| file_allocate_all_test(tr, 2, 0, 1, "test1", FILE_OPEN_CREATE); |
| } |
| |
| static void file_allocate_all_8tr_1_test(struct transaction* tr) { |
| file_allocate_all_test(tr, 8, 0, 1, "test1", FILE_OPEN_CREATE); |
| } |
| |
| static void file_allocate_all_complete1_test(struct transaction* tr) { |
| file_allocate_all_test(tr, 1, 1, 1, "test1", FILE_OPEN_CREATE); |
| } |
| |
| static void file_allocate_all_complete_multi1_test(struct transaction* tr) { |
| file_allocate_all_test(tr, 2, 8, 10, "test1", FILE_OPEN_CREATE); |
| } |
| |
| static void file_allocate_leave_10_test(struct transaction* tr) { |
| file_allocate_all_test(tr, 1, 1, 10, "test1", FILE_OPEN_CREATE); |
| } |
| |
| static void fs_create_checkpoint(struct transaction* tr) { |
| file_test(tr, "test_checkpoint", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| transaction_complete_update_checkpoint(tr); |
| assert(!tr->failed); |
| |
| transaction_activate(tr); |
| assert(get_fs_checkpoint_count(tr) > 3); |
| file_delete(tr, "test_checkpoint", false); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| |
| transaction_activate(tr); |
| assert(get_fs_checkpoint_count(tr) > 3); |
| } |
| |
| static void fs_modify_with_checkpoint(struct transaction* tr) { |
| file_test(tr, "test_checkpoint", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| transaction_complete_update_checkpoint(tr); |
| assert(!tr->failed); |
| |
| /* modify the active filesystem with an active checkpoint */ |
| transaction_activate(tr); |
| file_test(tr, "test_checkpoint", FILE_OPEN_NO_CREATE, file_test_block_count, |
| 0, 0, false, 2); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| |
| transaction_activate(tr); |
| assert(get_fs_checkpoint_count(tr) > 3); |
| file_delete(tr, "test_checkpoint", false); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| |
| transaction_activate(tr); |
| assert(get_fs_checkpoint_count(tr) > 3); |
| } |
| |
| static void fs_clear_checkpoint(struct transaction* tr) { |
| assert(get_fs_checkpoint_count(tr) > 3); |
| |
| file_delete(tr, "test_checkpoint", false); |
| /* at this point the file-system should be empty, all files are deleted */ |
| transaction_complete_update_checkpoint(tr); |
| |
| /* |
| * one block each for the checkpoint metadata block, checkpoint file tree |
| * root, and checkpoint free set, no file blocks should exist now |
| */ |
| assert(get_fs_checkpoint_count(tr) == 3); |
| |
| transaction_activate(tr); |
| } |
| |
| static void fs_rebuild_free_set(struct transaction* tr) { |
| data_block_t block; |
| ssize_t initial_free_count, free_count; |
| bool pending_modifications = false; |
| |
| check_fs_prepare(tr); |
| |
| initial_free_count = 0; |
| block = block_set_find_next_block(tr, &tr->fs->free, 1, true); |
| while (block) { |
| initial_free_count++; |
| block = block_set_find_next_block(tr, &tr->fs->free, block + 1, true); |
| } |
| block = block_set_find_next_block(tr, &tr->allocated, 1, true); |
| while (block) { |
| /* we allocated blocks already in this transaction */ |
| pending_modifications = true; |
| initial_free_count--; |
| block = block_set_find_next_block(tr, &tr->allocated, block + 1, true); |
| } |
| assert(initial_free_count > 0); |
| |
| if (print_test_verbose) { |
| printf("files before rebuild:\n"); |
| block_tree_print(tr, &tr->fs->files); |
| printf("free set before rebuild:\n"); |
| block_set_print(tr, &tr->fs->free); |
| } |
| |
| tr->rebuild_free_set = true; |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| if (print_test_verbose) { |
| printf("files after rebuild:\n"); |
| block_tree_print(tr, &tr->fs->files); |
| printf("free set after rebuild:\n"); |
| block_set_print(tr, &tr->fs->free); |
| } |
| |
| free_count = 0; |
| block = block_set_find_next_block(tr, &tr->fs->free, 1, true); |
| while (block) { |
| free_count++; |
| /* |
| * free the old free set nodes, because we should have replaced them |
| * with the rebuilt free set tree |
| */ |
| if (!block_set_replace_used_by(block, "free_tree_node", "free", 0, |
| false)) { |
| /* |
| * Ensure that all blocks in the new free set were in the old free |
| * set. We can only do this if there were no pending modifications |
| * in the transaction; if there were the files tree has been |
| * re-written and some blocks in the new free set will have been in |
| * the previous file tree. |
| */ |
| if (!pending_modifications) { |
| block_set_used_by(block, "free", 0); |
| } |
| } |
| block = block_set_find_next_block(tr, &tr->fs->free, block + 1, true); |
| } |
| |
| assert(free_count == initial_free_count); |
| } |
| |
| static void fs_rebuild_fragmented_free_set(struct transaction* tr) { |
| char path[10]; |
| int i; |
| for (i = 0; i < file_test_many_file_count; i++) { |
| snprintf(path, sizeof(path), "test%d", i); |
| file_test(tr, path, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 7 + i); |
| } |
| |
| /* |
| * Delete half the files, leaving the free set fragmented so we have a free |
| * set tree with depth > 1. |
| */ |
| for (i = 0; i < file_test_many_file_count; i += 2) { |
| snprintf(path, sizeof(path), "test%d", i); |
| file_test(tr, path, false, 0, 1, 1, true, 7 + i); |
| } |
| |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| fs_rebuild_free_set(tr); |
| |
| /* clean up */ |
| for (i = 0; i < file_test_many_file_count; i++) { |
| snprintf(path, sizeof(path), "test%d", i); |
| file_delete(tr, path, false); |
| } |
| } |
| |
| static void fs_rebuild_with_pending_file(struct transaction* tr) { |
| file_test(tr, "test_rebuild", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| fs_rebuild_free_set(tr); |
| file_delete(tr, "test_rebuild", false); |
| } |
| |
| static void fs_rebuild_with_pending_transaction(struct transaction* tr) { |
| struct transaction other_tr; |
| transaction_init(&other_tr, tr->fs, true); |
| fs_rebuild_free_set(tr); |
| assert(other_tr.failed); |
| transaction_free(&other_tr); |
| } |
| |
| static void fs_repair_flag(struct transaction* tr) { |
| enum file_op_result result; |
| struct storage_file_handle file; |
| |
| /* clear FS to reset repair flag */ |
| transaction_fail(tr); |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| |
| /* a non-existent file should not return FS_REPAIRED */ |
| result = file_open(tr, "test_simulated_repair_nonexistent", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(result == FILE_OP_ERR_NOT_FOUND); |
| |
| /* simulate an operation that requires setting the repair flag */ |
| file_test(tr, "test_simulated_repair", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| tr->repaired = true; |
| transaction_complete(tr); |
| assert(!tr->failed); |
| assert(fs_is_repaired(tr->fs)); |
| transaction_activate(tr); |
| |
| /* globally acknowledge repair in test helpers */ |
| allow_repaired = true; |
| |
| /* and again */ |
| file_test(tr, "test_simulated_repair", FILE_OPEN_NO_CREATE, |
| file_test_block_count, 0, 0, false, 2); |
| tr->repaired = true; |
| transaction_complete(tr); |
| assert(!tr->failed); |
| assert(fs_is_repaired(tr->fs)); |
| transaction_activate(tr); |
| |
| /* Opening existing files must ack repair, because we reset the whole FS */ |
| result = file_open(tr, "test_simulated_repair", &file, FILE_OPEN_NO_CREATE, |
| false); |
| assert(result == FILE_OP_ERR_FS_REPAIRED); |
| |
| /* a non-existent file should now report FS_REPAIRED */ |
| result = file_open(tr, "test_simulated_repair_nonexistent", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(result == FILE_OP_ERR_FS_REPAIRED); |
| |
| /* ...unless we allow a repaired FS */ |
| result = file_open(tr, "test_simulated_repair_nonexistent", &file, |
| FILE_OPEN_NO_CREATE, true); |
| assert(result == FILE_OP_ERR_NOT_FOUND); |
| |
| result = file_open(tr, "test_simulated_repair", &file, FILE_OPEN_NO_CREATE, |
| true); |
| assert(result == FILE_OP_SUCCESS); |
| file_close(&file); |
| |
| result = file_open(tr, "test_simulated_repair_nonexistent", &file, |
| FILE_OPEN_CREATE, true); |
| assert(result == FILE_OP_SUCCESS); |
| file_close(&file); |
| |
| /* |
| * re-initialize the fs to make sure we propagate the repaired state through |
| * the super block |
| */ |
| transaction_fail(tr); |
| block_test_reinit(tr, FS_INIT_FLAGS_NONE); |
| assert(fs_is_repaired(tr->fs)); |
| transaction_complete(tr); |
| |
| /* disallow repair globally in test helpers */ |
| allow_repaired = false; |
| |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| assert(!fs_is_repaired(tr->fs)); |
| /* force the cleared superblock to be written */ |
| file_test(tr, "test_simulated_repair", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| transaction_complete(tr); |
| |
| /* did the cleared repair flag persist? */ |
| block_test_reinit(tr, FS_INIT_FLAGS_NONE); |
| assert(!fs_is_repaired(tr->fs)); |
| |
| file_delete(tr, "test_simulated_repair", false); |
| } |
| |
| /* |
| * We don't allow repairs of the alternate FS, but we must persist it from the |
| * main FS across usage of the alternate. |
| */ |
| static void fs_repair_with_alternate(struct transaction* tr) { |
| enum file_op_result result; |
| struct storage_file_handle file; |
| |
| /* simulate an operation that requires setting the repair flag */ |
| file_test(tr, "test_repair_with_alternate", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| tr->repaired = true; |
| transaction_complete(tr); |
| assert(!tr->failed); |
| assert(fs_is_repaired(tr->fs)); |
| transaction_activate(tr); |
| |
| /* |
| * re-initialize the fs to make sure we propagate the repaired state through |
| * the super block |
| */ |
| transaction_fail(tr); |
| block_test_swap_clear_reinit( |
| tr, FS_INIT_FLAGS_DO_CLEAR | FS_INIT_FLAGS_ALTERNATE_DATA); |
| assert(tr->fs->main_repaired); |
| assert(!fs_is_repaired(tr->fs)); |
| |
| /* Opening a non-existent file does not return FILE_OP_ERR_FS_REPAIRED */ |
| result = file_open(tr, "test_alternate_nonexistent", &file, |
| FILE_OPEN_NO_CREATE, true); |
| assert(result == FILE_OP_ERR_NOT_FOUND); |
| |
| /* Make sure we rewrite the alternate superblock */ |
| file_test(tr, "test_alternate_create", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, true, 1); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| /* But we can't do a repair on the alternate FS */ |
| file_test(tr, "test_alternate_repair", FILE_OPEN_CREATE_EXCLUSIVE, |
| file_test_block_count, 0, 0, false, 1); |
| tr->repaired = true; |
| transaction_complete(tr); |
| assert(tr->failed); |
| assert(!fs_is_repaired(tr->fs)); |
| |
| /* Back to the main FS, which must still be repaired */ |
| block_test_swap_reinit(tr, FS_INIT_FLAGS_NONE); |
| assert(fs_is_repaired(tr->fs)); |
| transaction_complete(tr); |
| |
| /* Clear the repair flag */ |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| assert(!fs_is_repaired(tr->fs)); |
| } |
| |
| static void future_fs_version_test(struct transaction* tr) { |
| struct obj_ref super_ref = OBJ_REF_INITIAL_VALUE(super_ref); |
| struct fs* fs = tr->fs; |
| const struct key* key = fs->key; |
| struct block_device* dev = fs->dev; |
| struct block_device* super_dev = fs->super_dev; |
| const void* super_ro; |
| uint16_t* super_rw; |
| data_block_t block; |
| int ret; |
| struct storage_file_handle file; |
| enum file_op_result open_result; |
| |
| /* offset of fs_version field in uint16_t words */ |
| size_t fs_version_offset = 28 / 2; |
| |
| file_test(tr, "future_fs_version_file", FILE_OPEN_CREATE_EXCLUSIVE, 0, 0, 0, |
| false, 1); |
| |
| transaction_complete(tr); |
| |
| block = tr->fs->super_block[fs->super_block_version & 1]; |
| super_ro = block_get_super(fs, block, &super_ref); |
| assert(super_ro); |
| super_rw = block_dirty(tr, super_ro, false); |
| assert(super_rw); |
| super_rw[fs_version_offset]++; |
| block_put_dirty_no_mac(super_rw, &super_ref, false); |
| block_cache_clean_transaction(tr); |
| |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_NONE); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "future_fs_version_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_DO_CLEAR); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "future_fs_version_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| /* |
| * fs is not mountable, but we want to rewrite a block. Set up the bare |
| * minimum required fs so we can rewrite the superblock manually. |
| */ |
| fs->dev = dev; |
| fs->super_dev = super_dev; |
| fs->readable = true; |
| fs->writable = true; |
| |
| transaction_init(tr, fs, false); |
| super_ro = block_get_super(fs, block, &super_ref); |
| assert(super_ro); |
| super_rw = block_dirty(tr, super_ro, false); |
| assert(super_rw); |
| super_rw[fs_version_offset]--; |
| block_put_dirty_no_mac(super_rw, &super_ref, false); |
| block_cache_clean_transaction(tr); |
| transaction_free(tr); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_NONE); |
| assert(ret == 0); |
| |
| transaction_init(tr, fs, true); |
| |
| file_test(tr, "future_fs_version_file", FILE_OPEN_NO_CREATE, 0, 0, 0, |
| true /* delete */, 1); |
| } |
| |
| /** |
| * set_required_flags - helper to modify the required_flags super block field |
| * @fs: File system object. |
| * @required_flags: Flags to write into the required_flags field. |
| * |
| * Write @required_flags into the required_flags field of the active super |
| * block. @fs may have been destroyed with fs_destroy() but @fs->dev and |
| * @fs->super_dev must be reset correctly after it was destroyed. |
| * |
| * Returns: the previous required_flags value of the active super block. |
| */ |
| static uint16_t set_required_flags(struct fs* fs, uint16_t required_flags) { |
| /* offset of required_flags field in uint16_t words */ |
| size_t required_flags_offset = 30 / 2; |
| |
| struct obj_ref super_ref = OBJ_REF_INITIAL_VALUE(super_ref); |
| struct transaction tr; |
| struct block_device* dev = fs->dev; |
| const void* super_ro; |
| uint16_t* super_rw; |
| data_block_t block; |
| uint16_t old_required_flags; |
| |
| /* |
| * If the fs was mounted read-only due to an error, we need to override this |
| * state. We want to manually rewrite the superblock, so we have to override |
| * the read-only state for block_dirty() to be allowed. |
| */ |
| transaction_init(&tr, fs, false); |
| block = fs->super_block[fs->super_block_version & 1]; |
| super_ro = block_get_super(fs, block, &super_ref); |
| assert(super_ro); |
| super_rw = block_dirty(&tr, super_ro, false); |
| assert(super_rw); |
| old_required_flags = super_rw[required_flags_offset]; |
| super_rw[required_flags_offset] = required_flags; |
| block_put_dirty_no_mac(super_rw, &super_ref, false); |
| block_cache_clean_transaction(&tr); |
| transaction_free(&tr); |
| block_cache_dev_destroy(dev); |
| return old_required_flags; |
| } |
| |
| static void unknown_required_flags_test(struct transaction* tr) { |
| struct fs* fs = tr->fs; |
| const struct key* key = fs->key; |
| struct block_device* dev = fs->dev; |
| struct block_device* super_dev = fs->super_dev; |
| int ret; |
| uint16_t initial_required_flags; |
| struct storage_file_handle file; |
| enum file_op_result open_result; |
| |
| /* update when SUPER_BLOCK_REQUIRED_FLAGS_MASK changes in super.c */ |
| uint16_t first_unsupported_required_flag = 0x2U; |
| |
| file_test(tr, "unknown_flags_file", FILE_OPEN_CREATE_EXCLUSIVE, 0, 0, 0, |
| false, 1); |
| |
| transaction_complete(tr); |
| transaction_free(tr); |
| |
| initial_required_flags = |
| set_required_flags(fs, first_unsupported_required_flag); |
| |
| fs_destroy(fs); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_NONE); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "unknown_flags_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_DO_CLEAR); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "unknown_flags_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| /* |
| * fs is not mountable, but we want to rewrite a block. Set up the bare |
| * minimum required fs so we can rewrite the superblock manually. |
| */ |
| fs->dev = dev; |
| fs->super_dev = super_dev; |
| fs->readable = true; |
| fs->writable = true; |
| |
| /* set all flag bits, this should fail unless we support 16 flags */ |
| set_required_flags(fs, UINT16_MAX); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_NONE); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "unknown_flags_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_DO_CLEAR); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "unknown_flags_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| fs->dev = dev; |
| fs->super_dev = super_dev; |
| fs->readable = true; |
| fs->writable = true; |
| |
| /* set highest flag bit, this should fail unless we support 16 flags */ |
| set_required_flags(fs, 0x1U << 15); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_NONE); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "unknown_flags_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_DO_CLEAR); |
| assert(ret == 0); |
| assert(!fs_is_readable(fs)); |
| expect_errors(TRUSTY_STORAGE_ERROR_SUPERBLOCK_INVALID, 1); |
| |
| transaction_init(tr, fs, true); |
| open_result = file_open(tr, "unknown_flags_file", &file, |
| FILE_OPEN_NO_CREATE, false); |
| assert(open_result == FILE_OP_ERR_FAILED); |
| transaction_fail(tr); |
| transaction_free(tr); |
| fs_destroy(fs); |
| block_cache_dev_destroy(dev); |
| |
| fs->dev = dev; |
| fs->super_dev = super_dev; |
| fs->readable = true; |
| fs->writable = true; |
| |
| set_required_flags(fs, initial_required_flags); |
| |
| ret = fs_init(fs, FILE_SYSTEM_TEST, key, dev, super_dev, |
| FS_INIT_FLAGS_NONE); |
| assert(ret == 0); |
| |
| transaction_init(tr, fs, true); |
| |
| file_test(tr, "unknown_flags_file", FILE_OPEN_NO_CREATE, 0, 0, 0, |
| true /* delete */, 1); |
| } |
| |
| typedef data_block_t (*block_selector)(struct transaction* tr, |
| unsigned int arg); |
| |
| static void fs_corruption_helper(struct transaction* tr, |
| block_selector callback, |
| unsigned int arg, |
| bool expect_missing_file) { |
| struct storage_file_handle file; |
| enum file_op_result result; |
| struct fs* fs = tr->fs; |
| |
| file_test(tr, "recovery", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, |
| 0, 0, false, 1); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| open_test_file_etc(tr, &file, "recovery", FILE_OPEN_NO_CREATE, |
| FILE_OP_SUCCESS); |
| file_close(&file); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| /* Corrupt the provided block */ |
| memset(&blocks[callback(tr, arg)], 0, sizeof(struct block)); |
| block_cache_dev_destroy(fs->dev); |
| |
| result = file_open(tr, "recovery", &file, FILE_OPEN_NO_CREATE, false); |
| if (expect_missing_file) { |
| assert(result == FILE_OP_ERR_FAILED); |
| } else { |
| assert(result == FILE_OP_SUCCESS); |
| file_close(&file); |
| } |
| transaction_complete(tr); |
| assert(!expect_missing_file || tr->failed); |
| |
| /* re-initialize the filesystem without recovery enabled */ |
| block_test_reinit(tr, FS_INIT_FLAGS_NONE); |
| |
| open_test_file_etc( |
| tr, &file, "recovery", FILE_OPEN_NO_CREATE, |
| expect_missing_file ? FILE_OP_ERR_FAILED : FILE_OP_SUCCESS); |
| if (!expect_missing_file) { |
| file_close(&file); |
| } |
| transaction_complete(tr); |
| } |
| |
| static data_block_t select_files_block(struct transaction* tr, |
| unsigned int depth) { |
| struct block_tree_path path; |
| block_tree_walk(tr, &tr->fs->files, 0, true, &path); |
| assert(path.count > depth); |
| return block_mac_to_block(tr, &path.entry[depth].block_mac); |
| } |
| |
| static data_block_t select_free_block(struct transaction* tr, |
| unsigned int depth) { |
| struct block_tree_path path; |
| block_tree_walk(tr, &tr->fs->free.block_tree, 0, true, &path); |
| assert(path.count > depth); |
| return block_mac_to_block(tr, &path.entry[depth].block_mac); |
| } |
| |
| static data_block_t select_data_block(struct transaction* tr, |
| unsigned int block) { |
| struct storage_file_handle file; |
| const void* block_data_ro = NULL; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| data_block_t data_block_num; |
| |
| open_test_file_etc(tr, &file, "recovery", FILE_OPEN_NO_CREATE, |
| FILE_OP_SUCCESS); |
| |
| block_data_ro = file_get_block(tr, &file, block, &ref); |
| assert(block_data_ro); |
| data_block_num = data_to_block_num(block_data_ro - sizeof(struct iv)); |
| file_block_put(block_data_ro, &ref); |
| file_close(&file); |
| return data_block_num; |
| } |
| |
| static void create_and_delete(struct transaction* tr, const char* filename) { |
| struct storage_file_handle file; |
| open_test_file_etc(tr, &file, filename, FILE_OPEN_CREATE_EXCLUSIVE, |
| FILE_OP_SUCCESS); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| file_close(&file); |
| transaction_activate(tr); |
| file_delete(tr, filename, false); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| } |
| |
| static void fs_recovery_clear_roots_test(struct transaction* tr) { |
| /* |
| * Create and delete a file to ensure that we have a root files block and |
| * not just an empty super block. |
| */ |
| create_and_delete(tr, "ensure_roots"); |
| assert(!tr->fs->needs_full_scan); |
| |
| /* Corrupt the root files block */ |
| assert(select_files_block(tr, 0) == |
| block_mac_to_block(tr, &tr->fs->files.root)); |
| fs_corruption_helper(tr, select_files_block, 0, true); |
| assert(tr->failed); |
| /* |
| * fs_corruption_helper hits the corrupted block while checking the file, |
| * again while re-initializing the FS, and then again checking the file. |
| */ |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 3); |
| |
| assert(fs_check_full(tr->fs) == FS_CHECK_INVALID_BLOCK); |
| assert(tr->fs->needs_full_scan); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 2); |
| |
| /* re-initialize the filesystem with recovery enabled */ |
| block_test_reinit(tr, FS_INIT_FLAGS_RECOVERY_CLEAR_ALLOWED); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 1); |
| |
| /* Did we recover correctly? */ |
| create_and_delete(tr, "recovery"); |
| assert(!tr->fs->needs_full_scan); |
| |
| /* Corrupt the root of the free list */ |
| assert(select_free_block(tr, 0) == |
| block_mac_to_block(tr, &tr->fs->free.block_tree.root)); |
| fs_corruption_helper(tr, select_free_block, 0, false); |
| assert(tr->failed); |
| assert(tr->invalid_block_found); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 3); |
| |
| assert(fs_check_full(tr->fs) == FS_CHECK_INVALID_FREE_SET); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 1); |
| |
| /* re-initialize the filesystem with recovery enabled */ |
| block_test_reinit(tr, FS_INIT_FLAGS_RECOVERY_CLEAR_ALLOWED); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 1); |
| |
| /* Did we recover correctly? */ |
| create_and_delete(tr, "recovery"); |
| assert(!tr->invalid_block_found); |
| assert(!tr->fs->needs_full_scan); |
| } |
| |
| static void fs_check_file_child_test(struct transaction* tr) { |
| /* Create lots of files */ |
| file_create_many_test(tr); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| assert(!tr->fs->needs_full_scan); |
| |
| /* Corrupt a child in the files list */ |
| fs_corruption_helper(tr, select_files_block, 1, false); |
| /* |
| * We corrupt a file tree block, but the file that fs_corruption_helper |
| * probes is linked in a different file tree leaf and is unaffected by this, |
| * so the transaction succeeds. |
| */ |
| |
| /* Ensure that we detect this corruption */ |
| assert(fs_check_full(tr->fs) == FS_CHECK_INVALID_BLOCK); |
| assert(tr->fs->needs_full_scan); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 2); |
| |
| /* re-initialize the filesystem with recovery enabled */ |
| block_test_reinit(tr, FS_INIT_FLAGS_RECOVERY_CLEAR_ALLOWED); |
| |
| /* recovery doesn't fix this error */ |
| assert(fs_check_full(tr->fs) == FS_CHECK_INVALID_BLOCK); |
| assert(tr->fs->needs_full_scan); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 2); |
| |
| transaction_fail(tr); |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| |
| assert(fs_check_full(tr->fs) == FS_CHECK_NO_ERROR); |
| assert(!tr->fs->needs_full_scan); |
| } |
| |
| static void fs_check_free_child_test(struct transaction* tr) { |
| /* Fragment the free list */ |
| allocate_frag_test(tr); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| assert(!tr->fs->needs_full_scan); |
| |
| /* Corrupt a child in the free list */ |
| fs_corruption_helper(tr, select_free_block, 1, false); |
| assert(tr->failed); |
| assert(tr->invalid_block_found); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 4); |
| |
| /* Ensure that we detect this corruption */ |
| assert(fs_check_full(tr->fs) == FS_CHECK_INVALID_FREE_SET); |
| assert(tr->fs->needs_full_scan); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 2); |
| |
| /* re-initialize the filesystem with recovery enabled */ |
| block_test_reinit(tr, FS_INIT_FLAGS_RECOVERY_CLEAR_ALLOWED); |
| |
| /* recovery clear doesn't fix this error */ |
| assert(fs_check_full(tr->fs) == FS_CHECK_INVALID_FREE_SET); |
| assert(tr->fs->needs_full_scan); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 2); |
| |
| transaction_fail(tr); |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| |
| assert(fs_check_full(tr->fs) == FS_CHECK_NO_ERROR); |
| assert(!tr->fs->needs_full_scan); |
| } |
| |
| static void fs_check_sparse_file_test(struct transaction* tr) { |
| struct storage_file_handle file; |
| int i; |
| int* block_data_rw; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| size_t file_block_size = tr->fs->dev->block_size - sizeof(struct iv); |
| |
| open_test_file(tr, &file, "sparse_file", FILE_OPEN_CREATE); |
| for (i = 0; i < 20; i += 5) { |
| block_data_rw = file_get_block_write(tr, &file, i, true, &ref); |
| assert(block_data_rw); |
| |
| block_data_rw[0] = i; |
| block_data_rw[1] = ~i; |
| file_block_put_dirty(tr, &file, i, block_data_rw, &ref); |
| } |
| file_set_size(tr, &file, i * file_block_size); |
| |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| assert(fs_check_full(tr->fs) == FS_CHECK_NO_ERROR); |
| assert(!tr->fs->needs_full_scan); |
| |
| file_close(&file); |
| file_delete(tr, "sparse_file", false); |
| } |
| |
| static void fs_corrupt_data_blocks_test(struct transaction* tr) { |
| fs_corruption_helper(tr, select_data_block, 0, false); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| assert(fs_check_full(tr->fs) == FS_CHECK_NO_ERROR); |
| assert(!tr->fs->needs_full_scan); |
| |
| /* file should still exist because we don't scan data blocks */ |
| file_test(tr, "recovery", FILE_OPEN_NO_CREATE, 0, 0, 0, true, 1); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| assert(!tr->invalid_block_found); |
| transaction_activate(tr); |
| |
| /* we can delete files with corrupted data blocks */ |
| file_delete(tr, "recovery", false); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| transaction_activate(tr); |
| |
| create_and_delete(tr, "recovery"); |
| assert(!tr->invalid_block_found); |
| assert(!tr->fs->needs_full_scan); |
| } |
| |
| static void fs_persist_needs_full_scan_test(struct transaction* tr) { |
| const int* block_data_ro; |
| struct storage_file_handle file; |
| struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref); |
| |
| assert(!tr->fs->needs_full_scan); |
| transaction_complete(tr); |
| |
| /* clear the FS to get a known empty starting point */ |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| |
| /* Corrupt a data block */ |
| fs_corruption_helper(tr, select_data_block, 0, false); |
| assert(!tr->failed); |
| assert(!tr->fs->needs_full_scan); |
| |
| /* Try to read back the corrupted block */ |
| open_test_file(tr, &file, "recovery", FILE_OPEN_NO_CREATE); |
| block_data_ro = file_get_block(tr, &file, 0, &ref); |
| assert(!block_data_ro); |
| assert(tr->failed); |
| assert(tr->fs->needs_full_scan); |
| file_close(&file); |
| |
| /* Does the flag survive remount? */ |
| block_test_reinit(tr, FS_INIT_FLAGS_NONE); |
| assert(tr->fs->needs_full_scan); |
| transaction_complete(tr); |
| |
| /* And a switch back and forth to the alternate FS? */ |
| block_test_reinit(tr, FS_INIT_FLAGS_ALTERNATE_DATA); |
| assert(tr->fs->needs_full_scan); |
| transaction_complete(tr); |
| |
| block_test_reinit(tr, FS_INIT_FLAGS_NONE); |
| assert(tr->fs->needs_full_scan); |
| transaction_complete(tr); |
| |
| /* Clear the FS, clearing the scan flag. */ |
| block_test_reinit(tr, FS_INIT_FLAGS_DO_CLEAR); |
| assert(!tr->fs->needs_full_scan); |
| } |
| |
| static void fs_recovery_clear_test(struct transaction* tr) { |
| struct storage_file_handle file; |
| file_test(tr, "recovery", FILE_OPEN_CREATE_EXCLUSIVE, file_test_block_count, |
| 0, 0, false, 1); |
| transaction_complete(tr); |
| assert(!tr->failed); |
| |
| /* |
| * Backup, then clear and re-initialize the filesystem with only clear |
| * recovery enabled. |
| */ |
| block_test_swap_clear_reinit(tr, FS_INIT_FLAGS_RECOVERY_CLEAR_ALLOWED); |
| expect_errors(TRUSTY_STORAGE_ERROR_BLOCK_MAC_MISMATCH, 1); |
| |
| /* test file should be missing */ |
| open_test_file_etc(tr, &file, "recovery", FILE_OPEN_NO_CREATE, |
| FILE_OP_ERR_NOT_FOUND); |
| transaction_complete(tr); |
| |
|