diff --git a/tools/unit-tests/unit-mock-flash.c b/tools/unit-tests/unit-mock-flash.c index 66ec20d3..c0d29618 100644 --- a/tools/unit-tests/unit-mock-flash.c +++ b/tools/unit-tests/unit-mock-flash.c @@ -104,12 +104,40 @@ static void vault_restore_snapshot(const uint8_t *snapshot) #endif } -static void vault_flash_op(void) +/* Torn-operation mode. + * + * By default an injected fault abandons the whole flash operation, so the + * sector is either untouched or fully written -- which real silicon does + * not promise. With vault_powerfail_torn set, the faulting operation + * instead applies vault_torn_num/vault_torn_den of its bytes and only then + * loses power, leaving a half-erased or half-programmed sector behind. + */ +static int vault_powerfail_torn; +static int vault_torn_num = 1; +static int vault_torn_den = 2; + +/* How many of an operation's len bytes actually reach flash. + * + * Returns len when no fault is due on this operation. When one is: in the + * default atomic mode this longjmp()s and never returns, leaving flash + * untouched; in torn mode it returns a short count, and the caller applies + * that prefix and then calls vault_flash_torn_abort(). + */ +static int vault_flash_op_len(int len) { vault_flash_ops++; if ((vault_powerfail_at >= 0) && (vault_flash_ops > vault_powerfail_at)) { - longjmp(vault_powerfail_jmp, 1); + if (!vault_powerfail_torn) { + longjmp(vault_powerfail_jmp, 1); + } + return (int)(((long)len * vault_torn_num) / vault_torn_den); } + return len; +} + +static void vault_flash_torn_abort(void) +{ + longjmp(vault_powerfail_jmp, 1); } #endif @@ -149,13 +177,16 @@ int hal_flash_write(haladdr_t address, const uint8_t *data, int len) } #ifdef MOCK_KEYVAULT if ((address >= (const uintptr_t)vault_base) && (address < (const uintptr_t)vault_base + keyvault_size)) { - vault_flash_op(); + int n = vault_flash_op_len(len); #ifdef MOCK_STALE_CACHE a = vault_flash_at(address); #endif - for (i = 0; i < len; i++) { + for (i = 0; i < n; i++) { a[i] = data[i]; } + if (n != len) { + vault_flash_torn_abort(); + } } #endif #ifdef WOLFBOOT_DIAGNOSTICS_ADDRESS @@ -196,14 +227,17 @@ int hal_flash_erase(haladdr_t address, int len) memset((void *)(uintptr_t)address, 0xFF, len); #ifdef MOCK_KEYVAULT } else if ((address >= (uintptr_t)vault_base) && (address < (uintptr_t)vault_base + keyvault_size)) { - vault_flash_op(); + int n = vault_flash_op_len(len); printf("Erasing vault from %p : %p bytes\n", address, len); erased_vault++; #ifdef MOCK_STALE_CACHE - memset(vault_flash_at(address), 0xFF, len); + memset(vault_flash_at(address), 0xFF, n); #else - memset((void *)(uintptr_t)address, 0xFF, len); + memset((void *)(uintptr_t)address, 0xFF, n); #endif + if (n != len) { + vault_flash_torn_abort(); + } #endif #ifdef WOLFBOOT_DIAGNOSTICS_ADDRESS } else if ((address >= (haladdr_t)WOLFBOOT_DIAGNOSTICS_ADDRESS) && diff --git a/tools/unit-tests/unit-pkcs11_store.c b/tools/unit-tests/unit-pkcs11_store.c index 95d6c02c..186ffbf9 100644 --- a/tools/unit-tests/unit-pkcs11_store.c +++ b/tools/unit-tests/unit-pkcs11_store.c @@ -772,6 +772,19 @@ static int vault_obj_write(int type, CK_ULONG tok, CK_ULONG obj, * the former and fail on the latter. */ #define VAULT_OBJ_ABSENT (-1000) +/* Committed (on-flash) header size for an object, or -1 when the vault holds + * no node for it at all. Read after a power cycle, so the sector cache is + * empty and this is what actually survived in flash. */ +static int vault_obj_committed_size(int type, CK_ULONG tok, CK_ULONG obj) +{ + struct obj_hdr *hdr = find_object_header(type, (uint32_t)tok, + (uint32_t)obj); + + if (hdr == NULL) + return -1; + return (int)hdr->size; +} + static int vault_obj_read(int type, CK_ULONG tok, CK_ULONG obj, uint8_t *out, int max) { @@ -803,7 +816,11 @@ START_TEST (test_power_fail_during_rewrite_never_mixes_generations) { 2 * WOLFBOOT_SECTOR_SIZE]; const int type = DYNAMIC_TYPE_ECC; const CK_ULONG tok = 7, obj = 77; - int i, ret, ops, crash; + static const char *modestr[] = { "atomic", "torn 1/4", "torn 1/2", + "torn 3/4" }; + static const int torn_num[] = { 0, 1, 1, 3 }; + static const int torn_den[] = { 1, 4, 2, 4 }; + int i, ret, ops, crash, hdr_size, mode; for (i = 0; i < (int)sizeof(old_p); i++) old_p[i] = (uint8_t)('A' + (i % 23)); @@ -838,56 +855,76 @@ START_TEST (test_power_fail_during_rewrite_never_mixes_generations) { ck_assert_int_eq(ret, (int)sizeof(new_p)); ck_assert_mem_eq(rd, new_p, sizeof(new_p)); - for (crash = 0; crash <= ops; crash++) { - vault_restore_snapshot(snapshot); - vault_power_cycle(); - vault_flash_ops = 0; - vault_powerfail_at = crash; - if (setjmp(vault_powerfail_jmp) == 0) { - vault_obj_write(type, tok, obj, new_p, (int)sizeof(new_p)); - } - /* Power returns. */ - vault_powerfail_at = -1; - vault_power_cycle(); - memset(rd, 0, sizeof(rd)); - ret = vault_obj_read(type, tok, obj, rd, (int)sizeof(rd)); + /* Sweep every crash point once per fault shape: first with the faulting + * flash operation abandoned whole, then with it torn part-way through, + * so half-erased and half-programmed sectors are covered as well. Real + * silicon does not promise that a sector write is all-or-nothing. */ + for (mode = 0; mode < (int)(sizeof(torn_num) / sizeof(torn_num[0])); + mode++) { + vault_powerfail_torn = (mode != 0); + vault_torn_num = torn_num[mode]; + vault_torn_den = torn_den[mode]; + for (crash = 0; crash <= ops; crash++) { + vault_restore_snapshot(snapshot); + vault_power_cycle(); + vault_flash_ops = 0; + vault_powerfail_at = crash; + if (setjmp(vault_powerfail_jmp) == 0) { + vault_obj_write(type, tok, obj, new_p, (int)sizeof(new_p)); + } + /* Power returns. */ + vault_powerfail_at = -1; + vault_power_cycle(); + memset(rd, 0, sizeof(rd)); + ret = vault_obj_read(type, tok, obj, rd, (int)sizeof(rd)); - if (crash == ops) { - /* No fault can land on this iteration: vault_flash_op() only - * jumps once the op counter exceeds vault_powerfail_at, and a - * clean rewrite performs exactly ops operations. It is the - * no-fault control, so the rewrite ran to completion and the - * new payload must be there. Letting it take the empty/absent - * branch below would let a silently lost write pass. */ - ck_assert_msg(ret == (int)sizeof(new_p), - "no-fault control (op %d): object read back %d, expected " - "the new payload (%d bytes)", crash, ret, - (int)sizeof(new_p)); - ck_assert_msg(memcmp(rd, new_p, sizeof(new_p)) == 0, - "no-fault control (op %d): payload is not the new payload", - crash); - } - else if (ret == (int)sizeof(old_p)) { - ck_assert_msg(memcmp(rd, old_p, sizeof(old_p)) == 0, - "power fail at op %d: old-sized payload is not the old " - "payload", crash); - } - else if (ret == (int)sizeof(new_p)) { - ck_assert_msg(memcmp(rd, new_p, sizeof(new_p)) == 0, - "power fail at op %d: new-sized payload is not the new " - "payload", crash); - } - else { - /* Only two other outcomes are crash-safe: the object was never - * published, or it is present but truncated to empty by the - * Open-time durability commit. Every other return (a negative - * read error, or a partial payload length) means the vault came - * back damaged. */ - ck_assert_msg(ret == VAULT_OBJ_ABSENT || ret == 0, - "power fail at op %d: object read back %d, neither old " - "payload, new payload, empty, nor absent", crash, ret); + if (crash == ops) { + /* No fault can land on this iteration: vault_flash_op() only + * jumps once the op counter exceeds vault_powerfail_at, and a + * clean rewrite performs exactly ops operations. It is the + * no-fault control, so the rewrite ran to completion and the + * new payload must be there. Letting it take the empty/absent + * branch below would let a silently lost write pass. */ + ck_assert_msg(ret == (int)sizeof(new_p), + "%s no-fault control (op %d): object read back %d, " + "expected the new payload (%d bytes)", modestr[mode], + crash, ret, (int)sizeof(new_p)); + ck_assert_msg(memcmp(rd, new_p, sizeof(new_p)) == 0, + "%s no-fault control (op %d): payload is not the new " + "payload", modestr[mode], crash); + } + else if (ret == (int)sizeof(old_p)) { + ck_assert_msg(memcmp(rd, old_p, sizeof(old_p)) == 0, + "%s power fail at op %d: old-sized payload is not the old " + "payload", modestr[mode], crash); + } + else if (ret == (int)sizeof(new_p)) { + ck_assert_msg(memcmp(rd, new_p, sizeof(new_p)) == 0, + "%s power fail at op %d: new-sized payload is not the new " + "payload", modestr[mode], crash); + } + else { + /* The one crash-safe alternative to a whole generation is the + * truncated-but-present object the Open-time commit + * guarantees. The node must still be there -- the rewrite + * never calls create_object() for an existing object, so + * losing it outright (VAULT_OBJ_ABSENT) would be a real + * fault, not an empty + * rewrite -- and its committed size must be exactly the 8-byte + * tok/obj prefix, or the header itself came back torn. */ + ck_assert_msg(ret == 0, + "%s power fail at op %d: object read back %d, neither old " + "payload, new payload, nor empty", modestr[mode], crash, + ret); + hdr_size = vault_obj_committed_size(type, tok, obj); + ck_assert_msg(hdr_size == (int)(2 * sizeof(uint32_t)), + "%s power fail at op %d: empty read but committed header " + "size is %d, expected %d", modestr[mode], crash, hdr_size, + (int)(2 * sizeof(uint32_t))); + } } } + vault_powerfail_torn = 0; } END_TEST