mirror of https://github.com/wolfSSL/wolfBoot.git
pkcs11 store: cover torn flash ops and require a present, truncated header
The mock abandoned a faulting erase/program whole, so half-written sectors went untested; it can now tear one part-way and the sweep runs every crash point at 1/4, 1/2 and 3/4. An empty read must also leave the node in place with size 8, so a lost or torn header no longer passes as an empty rewrite.pull/873/head
parent
7919445c62
commit
ba544022e2
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue