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
Daniele Lacamera 2026-09-10 21:39:18 +02:00
parent 7919445c62
commit ba544022e2
2 changed files with 126 additions and 55 deletions

View File

@ -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) &&

View File

@ -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