| /* |
| * Copyright (C) 2021 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. |
| */ |
| |
| #define _GNU_SOURCE /* for asprintf */ |
| |
| #include <inttypes.h> |
| #include <libgen.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| |
| #include <trusty_unittest.h> |
| |
| #include "block_cache.h" |
| #include "block_device_tipc.h" |
| #include "crypt.h" |
| #include "file.h" |
| #include "rpmb.h" |
| #include "storageproxy_shim.h" |
| #include "transaction.h" |
| |
| /* For BLOCK_CACHE_SIZE */ |
| #include "block_cache_priv.h" |
| |
| static struct key storage_test_key; |
| static struct block_device_tipc test_block_device; |
| |
| static bool print_test_verbose = false; |
| |
| static inline void transaction_complete(struct transaction* tr) { |
| return transaction_complete_etc(tr, false); |
| } |
| |
| static void open_test_file_etc(struct transaction* tr, |
| struct file_handle* file, |
| const char* path, |
| enum file_create_mode create, |
| bool expect_failure) { |
| enum file_open_result result; |
| result = file_open(tr, path, file, create); |
| 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_EQ(result == FILE_OPEN_SUCCESS, !expect_failure); |
| ASSERT_EQ(true, result != FILE_OPEN_SUCCESS || |
| block_mac_valid(tr, &file->block_mac)); |
| |
| test_abort:; |
| } |
| |
| static void open_test_file(struct transaction* tr, |
| struct file_handle* file, |
| const char* path, |
| enum file_create_mode create) { |
| open_test_file_etc(tr, file, path, create, false); |
| } |
| |
| /* run tests on already open file */ |
| static void file_test_open(struct transaction* tr, |
| struct 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 (!block_data_rw) { |
| ASSERT_EQ(true, tr->failed); |
| goto test_abort; |
| } |
| if (print_test_verbose) { |
| printf("%s: allocate file block %d, %" PRIu64 ":\n", __func__, |
| i, data_to_block_num(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_GE(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_EQ(block_data_ro[0], i); |
| ASSERT_EQ(block_data_ro[1], ~i); |
| ASSERT_EQ(block_data_ro[2], id); |
| ASSERT_EQ(block_data_ro[3], ~id); |
| file_block_put((void*)block_data_ro - sizeof(struct iv), &ref); |
| } |
| ASSERT_EQ(i, read); |
| ASSERT_GE(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_EQ(i, free); |
| } |
| |
| test_abort:; |
| } |
| |
| static void file_test_commit(struct transaction* tr, bool commit) { |
| if (commit) { |
| transaction_complete(tr); |
| |
| ASSERT_EQ(false, tr->failed); |
| transaction_activate(tr); |
| } |
| |
| test_abort:; |
| } |
| |
| 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) { |
| bool deleted; |
| struct file_handle file; |
| |
| open_test_file(tr, &file, path, create); |
| ASSERT_EQ(false, HasFailure()); |
| if (tr->failed) { |
| goto test_abort; |
| } |
| |
| file_test_commit(tr, commit); |
| |
| if (move_path) { |
| file_move(tr, &file, move_path, move_create); |
| 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)); |
| } |
| deleted = file_delete(tr, path); |
| file_test_commit(tr, commit); |
| ASSERT_EQ(true, deleted); |
| } |
| |
| test_abort:; |
| 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 clear_all_pending_superblock_writes() { |
| fail_next_rpmb_writes(0, false); |
| |
| struct fs* fs = NULL; |
| |
| fs = &test_block_device.tr_state_rpmb; |
| if (fs->initial_super_block_tr) { |
| transaction_initial_super_block_complete(fs->initial_super_block_tr); |
| EXPECT_EQ(NULL, fs->initial_super_block_tr); |
| } |
| |
| #if HAS_FS_TDP |
| fs = &test_block_device.tr_state_ns_tdp; |
| if (fs->initial_super_block_tr) { |
| transaction_initial_super_block_complete(fs->initial_super_block_tr); |
| EXPECT_EQ(NULL, fs->initial_super_block_tr); |
| } |
| #endif |
| |
| fs = &test_block_device.tr_state_ns; |
| if (fs->initial_super_block_tr) { |
| transaction_initial_super_block_complete(fs->initial_super_block_tr); |
| EXPECT_EQ(NULL, fs->initial_super_block_tr); |
| } |
| } |
| |
| typedef struct transaction_test { |
| struct transaction tr; |
| int initial_super_block_version; |
| } StorageTest_t; |
| |
| #define IS_TP() (_state->tr.fs == &test_block_device.tr_state_rpmb) |
| |
| TEST_F_SETUP(StorageTest) { |
| fail_next_rpmb_writes(0, false); |
| transaction_init(&_state->tr, *((struct fs**)GetParam()), true); |
| _state->initial_super_block_version = _state->tr.fs->super_block_version; |
| } |
| |
| TEST_F_TEARDOWN(StorageTest) { |
| transaction_free(&_state->tr); |
| clear_all_pending_superblock_writes(); |
| } |
| |
| TEST_P(StorageTest, FileCreate) { |
| const char* filename = "FileCreate"; |
| struct file_handle file; |
| open_test_file(&_state->tr, &file, filename, FILE_OPEN_CREATE_EXCLUSIVE); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| file_close(&file); |
| |
| transaction_activate(&_state->tr); |
| open_test_file(&_state->tr, &file, filename, FILE_OPEN_NO_CREATE); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| file_close(&file); |
| |
| test_abort:; |
| } |
| |
| TEST_P(StorageTest, FailDataWrite) { |
| fail_next_rpmb_writes(1, false); |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| |
| /* force data block to be written */ |
| block_cache_clean_transaction(&_state->tr); |
| if (!IS_TP()) { |
| /* only superblock write should fail for TD */ |
| transaction_complete(&_state->tr); |
| ASSERT_NE(NULL, _state->tr.fs->initial_super_block_tr); |
| ASSERT_NE(_state->tr.fs->super_block_version, |
| _state->tr.fs->written_super_block_version); |
| } |
| ASSERT_EQ(true, _state->tr.failed); |
| transaction_activate(&_state->tr); |
| |
| /* did we recover? */ |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| |
| if (!IS_TP()) { |
| /* assert that we have overwritten the superblock */ |
| ASSERT_NE(_state->initial_super_block_version, |
| _state->tr.fs->super_block_version); |
| } |
| |
| test_abort:; |
| } |
| |
| TEST_P(StorageTest, FailDataWriteFullCache) { |
| fail_next_rpmb_writes(1, false); |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, |
| BLOCK_CACHE_SIZE + 1, 0, 0, false, 0); |
| /* |
| * We have queued more data than can fit in cache, we have to start flushing |
| * now. For TP this will immediately fail. |
| */ |
| |
| if (!IS_TP()) { |
| /* only superblock write should fail for TD */ |
| transaction_complete(&_state->tr); |
| ASSERT_NE(NULL, _state->tr.fs->initial_super_block_tr); |
| ASSERT_NE(_state->tr.fs->super_block_version, |
| _state->tr.fs->written_super_block_version); |
| } |
| ASSERT_EQ(true, _state->tr.failed); |
| transaction_activate(&_state->tr); |
| |
| /* did we recover? */ |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, |
| BLOCK_CACHE_SIZE + 1, 0, 0, false, 0); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| |
| if (!IS_TP()) { |
| /* assert that we have overwritten the superblock */ |
| ASSERT_NE(_state->initial_super_block_version, |
| _state->tr.fs->super_block_version); |
| } |
| |
| test_abort:; |
| } |
| |
| TEST_P(StorageTest, FailDataWriteWithCounterIncrement) { |
| fail_next_rpmb_writes(1, true); |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| |
| /* force data block to be written */ |
| block_cache_clean_transaction(&_state->tr); |
| if (!IS_TP()) { |
| /* only superblock write should fail for TD */ |
| transaction_complete(&_state->tr); |
| } |
| ASSERT_EQ(true, _state->tr.failed); |
| ASSERT_NE(NULL, _state->tr.fs->initial_super_block_tr); |
| ASSERT_NE(_state->tr.fs->super_block_version, |
| _state->tr.fs->written_super_block_version); |
| transaction_activate(&_state->tr); |
| |
| /* did we recover? */ |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| |
| block_cache_clean_transaction(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| /* assert that we have overwritten the superblock */ |
| ASSERT_NE(_state->initial_super_block_version, |
| _state->tr.fs->super_block_version); |
| |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| |
| test_abort:; |
| } |
| |
| TEST_P(StorageTest, FailDataWriteFullCacheWithIncrement) { |
| fail_next_rpmb_writes(1, true); |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, |
| BLOCK_CACHE_SIZE + 1, 0, 0, false, 0); |
| /* |
| * We have queued more data than can fit in cache, we have to start flushing |
| * now. For TP this will immediately fail. |
| */ |
| |
| if (!IS_TP()) { |
| /* only superblock write should fail for TD */ |
| transaction_complete(&_state->tr); |
| } |
| ASSERT_EQ(true, _state->tr.failed); |
| ASSERT_NE(NULL, _state->tr.fs->initial_super_block_tr); |
| ASSERT_NE(_state->tr.fs->super_block_version, |
| _state->tr.fs->written_super_block_version); |
| transaction_activate(&_state->tr); |
| |
| /* did we recover? */ |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, |
| BLOCK_CACHE_SIZE + 1, 0, 0, false, 0); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| |
| /* assert that we have overwritten the superblock */ |
| ASSERT_NE(_state->initial_super_block_version, |
| _state->tr.fs->super_block_version); |
| |
| test_abort:; |
| } |
| |
| /* |
| * Test that we don't crash the storage process if we fail to verify an RPMB |
| * write. This verify failure can occur if the storageproxy has shut down |
| * because a reboot is in progress, so we don't want to take down the entire |
| * device just because this happens. |
| */ |
| TEST_P(StorageTest, FailRpmbVerify) { |
| handle_t null_handle = 0; |
| int rc; |
| |
| file_test(&_state->tr, "FailRpmbVerifyValidFile", |
| FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 0); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| transaction_activate(&_state->tr); |
| |
| fail_next_rpmb_writes(1, true); |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| |
| /* force data block to be written */ |
| block_cache_clean_transaction(&_state->tr); |
| if (!IS_TP()) { |
| /* only superblock write should fail for TD */ |
| transaction_complete(&_state->tr); |
| } |
| ASSERT_EQ(true, _state->tr.failed); |
| ASSERT_NE(NULL, _state->tr.fs->initial_super_block_tr); |
| ASSERT_NE(_state->tr.fs->super_block_version, |
| _state->tr.fs->written_super_block_version); |
| transaction_activate(&_state->tr); |
| |
| /* Fail the verification that we actually performed the RPMB write */ |
| fail_next_rpmb_reads(1); |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| |
| /* Write will fail; first_write_complete is false and verification fails */ |
| block_cache_clean_transaction(&_state->tr); |
| if (!IS_TP()) { |
| /* only superblock write should fail for TD */ |
| transaction_complete(&_state->tr); |
| } |
| ASSERT_EQ(true, _state->tr.failed); |
| transaction_activate(&_state->tr); |
| |
| /* All RPMB access should now fail */ |
| file_test(&_state->tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, |
| 0); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(true, _state->tr.failed); |
| |
| /* |
| * de-initialize and re-initialize the block device to clear the |
| * verify_failed flag from the rpmb state. |
| */ |
| transaction_free(&_state->tr); |
| block_device_tipc_uninit(&test_block_device); |
| rc = block_device_tipc_init(&test_block_device, null_handle, |
| &storage_test_key, NULL, null_handle); |
| ASSERT_EQ(rc, 0); |
| transaction_init(&_state->tr, *((struct fs**)GetParam()), true); |
| |
| /* Everything should work now */ |
| file_test(&_state->tr, "FailRpmbVerifyValidFile", FILE_OPEN_NO_CREATE, 1, 0, |
| 0, true, 0); |
| transaction_complete(&_state->tr); |
| ASSERT_EQ(false, _state->tr.failed); |
| |
| test_abort:; |
| } |
| |
| TEST(StorageTest, FlushFailingSpecialTransaction) { |
| struct transaction td_tr; |
| struct transaction tp_tr; |
| transaction_init(&tp_tr, &test_block_device.tr_state_rpmb, true); |
| int tp_initial_super_block_version = tp_tr.fs->super_block_version; |
| |
| fail_next_rpmb_writes(1, true); |
| file_test(&tp_tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 0); |
| |
| /* force data block to be written */ |
| block_cache_clean_transaction(&tp_tr); |
| ASSERT_EQ(true, tp_tr.failed); |
| ASSERT_NE(NULL, tp_tr.fs->initial_super_block_tr); |
| ASSERT_NE(tp_tr.fs->super_block_version, |
| tp_tr.fs->written_super_block_version); |
| transaction_activate(&tp_tr); |
| |
| /* |
| * At this point there is a resync superblock write queueud up for each |
| * FS |
| */ |
| |
| fail_next_rpmb_writes(1, false); |
| transaction_init(&td_tr, &test_block_device.tr_state_ns, true); |
| file_test(&td_tr, "FlushFailingSpecialTransaction_td", |
| FILE_OPEN_CREATE_EXCLUSIVE, BLOCK_CACHE_SIZE, 0, 0, false, 0); |
| block_cache_clean_transaction(&td_tr); |
| /* |
| * This transaction will fail because we couldn't write the superblock to |
| * disk |
| */ |
| ASSERT_EQ(true, td_tr.failed); |
| transaction_activate(&td_tr); |
| |
| file_test(&td_tr, "FlushFailingSpecialTransaction_td", |
| FILE_OPEN_CREATE_EXCLUSIVE, BLOCK_CACHE_SIZE, 0, 0, false, 0); |
| block_cache_clean_transaction(&td_tr); |
| ASSERT_EQ(false, td_tr.failed); |
| transaction_complete(&td_tr); |
| ASSERT_EQ(false, td_tr.failed); |
| transaction_activate(&td_tr); |
| |
| /* TD resync is done, TP is pending */ |
| ASSERT_EQ(NULL, td_tr.fs->initial_super_block_tr); |
| ASSERT_NE(NULL, tp_tr.fs->initial_super_block_tr); |
| |
| /* Fill the cache up */ |
| file_test(&td_tr, "FlushFailingSpecialTransaction_td", FILE_OPEN_CREATE, |
| BLOCK_CACHE_SIZE + 1, 0, 0, false, 0); |
| ASSERT_EQ(false, td_tr.failed); |
| block_cache_clean_transaction(&td_tr); |
| ASSERT_EQ(false, td_tr.failed); |
| ASSERT_NE(NULL, tp_tr.fs->initial_super_block_tr); |
| |
| /* did we recover? */ |
| file_test(&tp_tr, __func__, FILE_OPEN_CREATE_EXCLUSIVE, 1, 0, 0, false, 0); |
| |
| block_cache_clean_transaction(&tp_tr); |
| ASSERT_EQ(false, tp_tr.failed); |
| /* assert that we have overwritten the superblock */ |
| ASSERT_NE(tp_initial_super_block_version, tp_tr.fs->super_block_version); |
| |
| transaction_complete(&tp_tr); |
| ASSERT_EQ(false, tp_tr.failed); |
| |
| test_abort:; |
| if (!tp_tr.failed) { |
| transaction_fail(&tp_tr); |
| } |
| if (!td_tr.failed) { |
| transaction_fail(&td_tr); |
| } |
| transaction_free(&tp_tr); |
| transaction_free(&td_tr); |
| clear_all_pending_superblock_writes(); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(Filesystem, |
| StorageTest, |
| testing_Values(&test_block_device.tr_state_rpmb, |
| &test_block_device.tr_state_ns)); |
| |
| int main(int argc, const char* argv[]) { |
| int rc = 1; |
| handle_t null_handle = 0; |
| char* exec_filename = NULL; |
| |
| crypt_init(); |
| block_cache_init(); |
| |
| exec_filename = strdup(argv[0]); |
| if (!exec_filename) { |
| goto err; |
| } |
| const char* dir = dirname(exec_filename); |
| if (!init_rpmb_state(dir)) { |
| goto err; |
| } |
| rc = block_device_tipc_init(&test_block_device, null_handle, |
| &storage_test_key, NULL, null_handle); |
| if (rc < 0) { |
| fprintf(stderr, "%s: block_device_tipc_init failed (%d)\n", __func__, |
| rc); |
| goto err; |
| } |
| |
| rc = RUN_ALL_TESTS() ? 0 : 1; |
| |
| block_device_tipc_uninit(&test_block_device); |
| |
| err: |
| crypt_shutdown(); |
| destroy_rpmb_state(); |
| if (exec_filename) |
| free(exec_filename); |
| |
| (void)file_test; |
| |
| return rc; |
| } |