fix(storage/nvs): Fixed hadling of inconsistent values in NVS entry header

feat(storage/nvs): Added test cases for damaged entries with correct CRC
This commit is contained in:
radek.tandler 2024-07-29 13:50:12 +02:00
parent d2cfb78d31
commit b937cb7549
5 changed files with 836 additions and 241 deletions

View File

@ -588,7 +588,6 @@ TEST_CASE("nvs api tests", "[nvs]")
const uint32_t NVS_FLASH_SECTOR_COUNT_MIN = 3;
TEMPORARILY_DISABLED(f.emu.setBounds(NVS_FLASH_SECTOR, NVS_FLASH_SECTOR + NVS_FLASH_SECTOR_COUNT_MIN);)
TEST_ESP_ERR(nvs_open("namespace1", NVS_READWRITE, &handle_1), ESP_ERR_NVS_NOT_INITIALIZED);
for (uint16_t i = NVS_FLASH_SECTOR; i < NVS_FLASH_SECTOR + NVS_FLASH_SECTOR_COUNT_MIN; ++i) {
f.erase(i);
@ -758,8 +757,7 @@ TEST_CASE("nvs iterators tests", "[nvs]")
int count = 0;
nvs_iterator_t it = nullptr;
esp_err_t res = nvs_entry_find(part, name, type, &it);
for (count = 0; res == ESP_OK; count++)
{
for (count = 0; res == ESP_OK; count++) {
res = nvs_entry_next(&it);
}
CHECK(res == ESP_ERR_NVS_NOT_FOUND); // after finishing the loop or if no entry was found to begin with,
@ -773,8 +771,7 @@ TEST_CASE("nvs iterators tests", "[nvs]")
int count = 0;
nvs_iterator_t it = nullptr;
esp_err_t res = nvs_entry_find_in_handle(handle, type, &it);
for (count = 0; res == ESP_OK; count++)
{
for (count = 0; res == ESP_OK; count++) {
res = nvs_entry_next(&it);
}
CHECK(res == ESP_ERR_NVS_NOT_FOUND); // after finishing the loop or if no entry was found to begin with,
@ -828,7 +825,6 @@ TEST_CASE("nvs iterators tests", "[nvs]")
CHECK(entry_count(NVS_DEFAULT_PART_NAME, NULL, NVS_TYPE_U64) == 1);
}
SECTION("Number of entries found for specified handle and type is correct") {
CHECK(entry_count_handle(handle_1, NVS_TYPE_ANY) == 11);
CHECK(entry_count_handle(handle_1, NVS_TYPE_I32) == 3);
@ -892,7 +888,6 @@ TEST_CASE("nvs iterators tests", "[nvs]")
nvs_release_iterator(it);
}
SECTION("Iterating over multiple pages works correctly") {
nvs_handle_t handle_3;
const char *name_3 = "namespace3";
@ -1242,8 +1237,7 @@ public:
static_assert(nKeys == sizeof(future_values) / sizeof(future_values[0]), "");
auto randomRead = [&](size_t index) -> esp_err_t {
switch (types[index])
{
switch (types[index]) {
case nvs::ItemType::I32: {
int32_t val;
auto err = nvs_get_i32(handle, keys[index], &val);
@ -1290,8 +1284,7 @@ public:
};
auto randomWrite = [&](size_t index) -> esp_err_t {
switch (types[index])
{
switch (types[index]) {
case nvs::ItemType::I32: {
int32_t val = static_cast<int32_t>(gen());
@ -1910,7 +1903,6 @@ TEST_CASE("Check that orphaned blobs are erased during init", "[nvs]")
TEST_ESP_OK(storage.writeItem(1, nvs::ItemType::BLOB, "key", blob, sizeof(blob)));
TEST_ESP_OK(storage.init(0, 5));
/* Check that multi-page item is still available.**/
TEST_ESP_OK(storage.readItem(1, nvs::ItemType::BLOB, "key", blob, sizeof(blob)));
@ -2162,7 +2154,6 @@ TEST_CASE("Recovery from power-off during modification of blob present in old-fo
size_t buflen = sizeof(hexdata);
uint8_t buf[nvs::Page::CHUNK_MAX_SIZE];
/* Power-off when blob was being written on the different page where its old version in old format
* was present*/
nvs::Page p;
@ -2639,6 +2630,499 @@ TEST_CASE("crc error in variable length item is handled", "[nvs]")
}
}
// handle damaged item header's span=0 even if crc is correct
TEST_CASE("zero span in item header with correct crc is handled", "[nvs]")
{
PartitionEmulationFixture f(0, 3);
nvs::Storage storage(f.part());
// prepare some data
TEST_ESP_OK(storage.init(0, 3));
TEST_ESP_OK(storage.writeItem(0, "ns1", static_cast<uint8_t>(1)));
TEST_ESP_OK(storage.writeItem(1, "value1", static_cast<uint32_t>(1)));
TEST_ESP_OK(storage.writeItem(1, "value2", static_cast<uint32_t>(2)));
// damage item header of value1 to introduce span==0 error, recalculate crc
{
uint8_t new_span = 0;
size_t entry_offset = 32 * 3; // 2x page header + 1x ns1
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
// set span to 0
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
// allow overwriting smaller portion of flash than whole 4k page
((esp_partition_t*) f.get_esp_partition())->erase_size = 1;
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
// check that storage can recover
uint32_t val = 0;
TEST_ESP_OK(storage.init(0, 3));
TEST_ESP_OK(storage.readItem(1, "value2", val));
CHECK(val == 2);
// check that the damaged item is no longer present
TEST_ESP_ERR(ESP_ERR_NVS_NOT_FOUND, storage.readItem(1, "value1", val));
// add more items named "item_0" till "item_125" to make the page full
for (size_t i = 0; i < nvs::Page::ENTRY_COUNT; ++i) {
char item_name[nvs::Item::MAX_KEY_LENGTH + 1];
snprintf(item_name, sizeof(item_name), "item_%ld", (long int)i);
TEST_ESP_OK(storage.writeItem(1, item_name, static_cast<uint32_t>(i)));
}
// damage item header of item_125 to introduce span==0 error, recalculate crc
{
uint8_t new_span = 0;
size_t entry_offset = 32 * (2 + 1 + 1 + 128) ; // 2x page header + 1x ns1 + 1x value1 + whole page
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
// set span to 0
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
// allow overwriting smaller portion of flash than whole 4k page
((esp_partition_t*) f.get_esp_partition())->erase_size = 1;
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
// check that storage can recover
TEST_ESP_OK(storage.init(0, 3));
// check that the damaged item is no longer present
TEST_ESP_ERR(ESP_ERR_NVS_NOT_FOUND, storage.readItem(1, "item_125", val));
}
// test case for damaged item header with correct crc using string.
// first sub-case, span goes over the remaining entries in the page
// second sub-case, span goes over the number of entries required to store the string
// third sub-case, span goes below the number of entries required to store the string
// fourth sub-case, indicated variable data length goes over the remaining space in the page
TEST_CASE("inconsistent fields in item header with correct crc are handled for string", "[nvs]")
{
PartitionEmulationFixture f(0, 3);
nvs::Storage storage(f.part());
// prepare some data
TEST_ESP_OK(storage.init(0, 3));
TEST_ESP_OK(storage.writeItem(0, "ns1", static_cast<uint8_t>(1)));
const char str[] = "String67890123456789012345678901String67890123456789012345678901String6789012345678901234567890"; // 95 + 1 bytes data occupy 3 entries, overhead 1
TEST_ESP_OK(storage.writeItem(1, nvs::ItemType::SZ, "valuestr1", str, strlen(str)));
TEST_ESP_OK(storage.writeItem(1, nvs::ItemType::SZ, "valuestr2", str, strlen(str)));
TEST_ESP_OK(storage.writeItem(1, nvs::ItemType::SZ, "valuestr3", str, strlen(str)));
TEST_ESP_OK(storage.writeItem(1, nvs::ItemType::SZ, "valuestr4", str, strlen(str)));
TEST_ESP_OK(storage.writeItem(1, "valueu32", static_cast<uint32_t>(2)));
// read the values back
char read_str[sizeof(str)] = {0};
size_t read_str_size = sizeof(read_str);
uint32_t val = 0;
TEST_ESP_OK(storage.readItem(1, nvs::ItemType::SZ, "valuestr1", (void*)read_str, read_str_size));
CHECK(strcmp(read_str, str) == 0);
TEST_ESP_OK(storage.readItem(1, nvs::ItemType::SZ, "valuestr2", (void*)read_str, read_str_size));
CHECK(strcmp(read_str, str) == 0);
TEST_ESP_OK(storage.readItem(1, nvs::ItemType::SZ, "valuestr3", (void*)read_str, read_str_size));
CHECK(strcmp(read_str, str) == 0);
TEST_ESP_OK(storage.readItem(1, nvs::ItemType::SZ, "valuestr4", (void*)read_str, read_str_size));
CHECK(strcmp(read_str, str) == 0);
// default values for re-check after sections of the test
esp_err_t exp_err_str1 = ESP_OK;
esp_err_t exp_err_str2 = ESP_OK;
esp_err_t exp_err_str3 = ESP_OK;
esp_err_t exp_err_str4 = ESP_OK;
TEST_ESP_OK(storage.readItem(1, "valueu32", val));
CHECK(val == 2);
// allow overwriting smaller portion of flash than whole 4k page
((esp_partition_t*) f.get_esp_partition())->erase_size = 1;
SECTION("damage item header of valuestr1 to introduce span exceeding remaining page size error") {
// span of the valstr1 is 4, there are 126 - 1 = 125 entries left in the page, set damaged span to 126
uint8_t new_span = 126;
const uint8_t entry_index = 3; // 2x page header + 1x ns1
size_t entry_offset = 32 * entry_index;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
// expect error when trying to read the item
exp_err_str1 = ESP_ERR_NVS_NOT_FOUND;
}
SECTION("damage item header of valuestr2 to introduce span exceeding the number of entries required to store the string") {
// span of the valstr2 is 4, there are 126 - (1 + 4) = 121 entries left in the page, set damaged span to value below 121 and above 4
uint8_t new_span = 10;
const uint8_t entry_index = 7; // 2x page header + 1x ns1 + 4x valuestr1
size_t entry_offset = 32 * entry_index;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
// expect error when trying to read the item
exp_err_str2 = ESP_ERR_NVS_NOT_FOUND;
}
SECTION("damage item header of valuestr3 to introduce span lower than the number of entries required to store the string") {
// span of the valstr3 is 4, set to 3
uint8_t new_span = 3;
const uint8_t entry_index = 11; // 2x page header + 1x ns1 + 4x valuestr1 + 4x valuestr2
size_t entry_offset = 32 * entry_index;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
// expect error when trying to read the item
exp_err_str3 = ESP_ERR_NVS_NOT_FOUND;
}
SECTION("damage item header of valuestr4 to introduce indicated variable data length going over the remaining space in the page") {
const uint8_t entry_index = 15; // 2x page header + 1x ns1 + 4x valuestr1 + 4x valuestr2 + 4x valuestr3
size_t entry_offset = 32 * entry_index;
// we are at entry 15, there are 126 - 15 = 111 entries left in the page, set data size to 112 * 32 bytes
uint16_t new_size = 112 * 32;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
item->varLength.dataSize = new_size;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
// expect error when trying to read the item
exp_err_str4 = ESP_ERR_NVS_NOT_FOUND;
}
// check that storage can recover
TEST_ESP_OK(storage.init(0, 3));
TEST_ESP_OK(storage.readItem(1, "valueu32", val));
CHECK(val == 2);
// check that the damaged items are no longer present
char read_buff[sizeof(str)];
TEST_ESP_ERR(exp_err_str1, storage.readItem(1, nvs::ItemType::SZ, "valuestr1", read_buff, sizeof(read_buff)));
TEST_ESP_ERR(exp_err_str2, storage.readItem(1, nvs::ItemType::SZ, "valuestr2", read_buff, sizeof(read_buff)));
TEST_ESP_ERR(exp_err_str3, storage.readItem(1, nvs::ItemType::SZ, "valuestr3", read_buff, sizeof(read_buff)));
TEST_ESP_ERR(exp_err_str4, storage.readItem(1, nvs::ItemType::SZ, "valuestr4", read_buff, sizeof(read_buff)));
}
// Inconsistent fields in item header with correct crc are handled for blobs
// Before each sub case damaging the blob with key = "valueblob1", following scheme will be used to store the blob and do the test
// Page 1
// 1x namespace entry
// 1x U32 entry, key = "valueu32_1" before data
// 124x BLOB_DATA entries, key = "valueblob1", chunk index = 0 containing 123*32 bytes of data
// Page 2
// 3x BLOB_DATA entries, key = "valueblob1", chunk index = 1 containing 2*32 bytes of data
// 1x BLOB_INDEX entry key = "valueblob1", chunkVersion = 0, chunkCount = 2
// 3x BLOB_DATA entries, key = "valueblob2", chunk index = 0 containing 2*32 bytes of data
// 1x BLOB_INDEX entry key = "valueblob2", chunkVersion = 0, chunkCount = 1
// 1x U32 entry key = "valueu32_2" after data
TEST_CASE("inconsistent fields in item header with correct crc are handled for multi page blob", "[nvs]")
{
PartitionEmulationFixture f(0, 3);
// keys
char ukey1[] = "valueu32_1";
char ukey2[] = "valueu32_2";
char blob_key1[] = "valueblob1";
char blob_key2[] = "valueblob2";
// planned chunk lengths
size_t blob_key1_chunk_0_len = 123 * 32;
size_t blob_key1_chunk_1_len = 2 * 32;
size_t blob_key2_chunk_len = 2 * 32;
// initial values
uint32_t uval1 = 1;
uint32_t uval2 = 2;
uint8_t blob_data1[blob_key1_chunk_0_len + blob_key1_chunk_1_len];
uint8_t blob_data2[blob_key2_chunk_len];
// value buffers
uint32_t read_u32_1;
uint32_t read_u32_2;
uint8_t read_blob1[sizeof(blob_data1)];
uint8_t read_blob2[sizeof(blob_data2)];
size_t read_blob1_size = sizeof(read_blob1);
size_t read_blob2_size = sizeof(read_blob2);
// Skip one page, 2 entries of page header and 3 entries of valueblob1's BLOB_DATA entries
const size_t blob_index_offset = 4096 + 32 * 2 + 32 * 3;
// Skip 2 entries of page header 1 entry of namespace and 1 entry of valueu32_1
const size_t blob_data_chunk0_offset = 32 * 2 + 32 * 1 + 32 * 1;
// returns length of BLOB_DATA of blob_key1 on page 1 within the partition
// Skip 2 entries of page header 1 entry of namespace and 1 entry of valueu32_1
const uint16_t blob_data_chunk0_len = blob_key1_chunk_0_len;
// returns length of BLOB_DATA of blob_key1 on page 1 within the partition
// Skip 2 entries of page header 1 entry of namespace and 1 entry of valueu32_1
const uint16_t blob_data_chunk1_len = blob_key1_chunk_1_len;
// returns offset of BLOB_DATA of blob_key1 on page 1 within the partition
// Skip one page, 2 entries of page header
const size_t blob_data_chunk1_offset = 4096 + 32 * 2;
// initialize buffers
read_u32_1 = 0;
read_u32_2 = 0;
memset(blob_data1, 0x55, sizeof(blob_data1));
memset(blob_data2, 0xaa, sizeof(blob_data2));
memset(read_blob1, 0, sizeof(read_blob1));
memset(read_blob2, 0, sizeof(read_blob2));
// Write initial data to the nvs partition
{
TEST_ESP_OK(nvs::NVSPartitionManager::get_instance()->init_custom(f.part(), 0, 3));
nvs_handle_t handle;
TEST_ESP_OK(nvs_open("test", NVS_READWRITE, &handle));
TEST_ESP_OK(nvs_set_u32(handle, ukey1, uval1));
TEST_ESP_OK(nvs_set_blob(handle, blob_key1, blob_data1, sizeof(blob_data1)));
TEST_ESP_OK(nvs_set_blob(handle, blob_key2, blob_data2, sizeof(blob_data2)));
TEST_ESP_OK(nvs_set_u32(handle, ukey2, uval2));
nvs_close(handle);
TEST_ESP_OK(nvs_flash_deinit_partition(f.part()->get_partition_name()));
}
// during the test, we will make changes of the data in more granular way than the whole 4k page
// allow overwriting smaller portion of flash than whole 4k page
((esp_partition_t*) f.get_esp_partition())->erase_size = 1;
// Sub test cases
SECTION("damage BLOB_IDX - span set to 0") {
// Damage BLOB_IDX of valueblob1, modify span == 1 to 0
size_t entry_offset = blob_index_offset;
uint8_t expected_span = 1;
uint8_t new_span = 0;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
CHECK(item->span == expected_span);
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
SECTION("damage BLOB_IDX - chunkIndex set to 111 instead of CHUNK_ANY") {
// Damage BLOB_IDX of valueblob1, modify chunkIndex == 111
size_t entry_offset = blob_index_offset;
uint8_t expected_chunk_index = nvs::Item::CHUNK_ANY;
uint8_t new_chunk_index = 111;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
CHECK(item->chunkIndex == expected_chunk_index);
item->chunkIndex = new_chunk_index;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
SECTION("damage BLOB_IDX - blobIndex.dataSize over theoretical limit of 32*125*127 (entry size * max payload entries in page * max # of chunks)") {
// Damage BLOB_IDX of valueblob1, modify blobIndex.dataSize
size_t entry_offset = blob_index_offset;
uint32_t expected_data_size = blob_data_chunk0_len + blob_data_chunk1_len;
uint32_t new_data_size = (32 * 125 * 127) + 1;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
CHECK(item->blobIndex.dataSize == expected_data_size);
item->chunkIndex = new_data_size;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
SECTION("damage BLOB_DATA - chunkIndex set to invalid value of CHUNK_ANY") {
// Damage BLOB_DATA of valueblob1, modify chunkIndex to CHUNK_ANY
size_t entry_offset = blob_data_chunk0_offset;
uint8_t unexpected_chunk_index = nvs::Item::CHUNK_ANY;
uint8_t new_chunk_index = nvs::Item::CHUNK_ANY;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
CHECK(item->chunkIndex != unexpected_chunk_index);
item->chunkIndex = new_chunk_index;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
SECTION("damage BLOB_DATA - varLength.dataSize is exceeding the maximum capacity available till the end of the page") {
// Damage BLOB_DATA of valueblob1, set new data size to be beyond remaining capacity in the page.
// Even if all entries are used for payload, there will be one overhead entry required
size_t entry_offset = blob_data_chunk1_offset; // let's take chunk1 for this test
uint16_t expected_data_size = blob_data_chunk1_len;
uint16_t new_data_size = 32 * 126;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
CHECK(item->varLength.dataSize == expected_data_size);
item->varLength.dataSize = new_data_size;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
SECTION("damage BLOB_DATA - span > maxAvailablePageSpan") {
// Damage BLOB_DATA of valueblob1, set span to the value larger than the rest of entries in the page but smaller than maximum of entries .
// page can accommodate
// chunk0 starts at entry index 2 so max span is 126 - 2 = 124. Use 125
size_t entry_offset = blob_data_chunk0_offset;
uint8_t expected_span = (blob_data_chunk0_len + 31) / 32 + 1;
uint8_t new_span = 125;
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
CHECK(item->span == expected_span);
item->span = new_span;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
// check that storage can recover and validate the remaining data
{
TEST_ESP_OK(nvs::NVSPartitionManager::get_instance()->init_custom(f.part(), 0, 3));
nvs_handle_t handle;
TEST_ESP_OK(nvs_open("test", NVS_READWRITE, &handle));
TEST_ESP_OK(nvs_get_u32(handle, ukey1, &read_u32_1));
TEST_ESP_ERR(nvs_get_blob(handle, blob_key1, &read_blob1, &read_blob1_size), ESP_ERR_NVS_NOT_FOUND);
TEST_ESP_OK(nvs_get_blob(handle, blob_key2, &read_blob2, &read_blob2_size));
TEST_ESP_OK(nvs_get_u32(handle, ukey2, &read_u32_2));
nvs_close(handle);
TEST_ESP_OK(nvs_flash_deinit_partition(f.part()->get_partition_name()));
// validate the data
CHECK(read_u32_1 == uval1);
CHECK(read_u32_2 == uval2);
CHECK(memcmp(read_blob2, blob_data2, sizeof(blob_data2)) == 0);
}
}
// header entry of u32 entry will be damaged in a way that data type will be invalid
// while crc will be correct
TEST_CASE("invalid data type in item header with correct crc is handled", "[nvs]")
{
PartitionEmulationFixture f(0, 3);
nvs::Storage storage(f.part());
// prepare some data
TEST_ESP_OK(storage.init(0, 3));
TEST_ESP_OK(storage.writeItem(0, "ns1", static_cast<uint8_t>(1)));
TEST_ESP_OK(storage.writeItem(1, "value1", static_cast<uint32_t>(1)));
TEST_ESP_OK(storage.writeItem(1, "value2", static_cast<uint32_t>(2)));
TEST_ESP_OK(storage.writeItem(1, "value3", static_cast<uint32_t>(3)));
// damage item header of value2 to introduce data type error, recalculate crc
{
nvs::ItemType new_data_type = (nvs::ItemType) 0xfe;
size_t entry_offset = 32 * 4; // 2x page header + 1x ns1 + 1x value1
uint8_t buff[sizeof(nvs::Item)] = {0};
TEST_ESP_OK(esp_partition_read(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
nvs::Item* item = (nvs::Item*)buff;
// set span to U8
item->datatype = new_data_type;
// recalculate crc same way as nvs::Item::calculateCrc32 does
item->crc32 = ((nvs::Item*)buff)->calculateCrc32();
// allow overwriting smaller portion of flash than whole 4k page
((esp_partition_t*) f.get_esp_partition())->erase_size = 1;
TEST_ESP_OK(esp_partition_erase_range(f.get_esp_partition(), entry_offset, sizeof(buff)));
TEST_ESP_OK(esp_partition_write(f.get_esp_partition(), entry_offset, &buff, sizeof(buff)));
}
// check that storage can recover
uint32_t val = 0;
TEST_ESP_OK(storage.init(0, 3));
TEST_ESP_OK(storage.readItem(1, "value1", val));
CHECK(val == 1);
// check that the damaged item is no longer present
TEST_ESP_ERR(ESP_ERR_NVS_NOT_FOUND, storage.readItem(1, "value2", val));
TEST_ESP_OK(storage.readItem(1, "value3", val));
CHECK(val == 3);
}
// leaks memory
TEST_CASE("Recovery from power-off when the entry being erased is not on active page", "[nvs]")
{
@ -3085,7 +3569,6 @@ TEST_CASE("check and read data from partition generated via manufacturing utilit
3,
"testdata/sample_singlepage_blob.bin");
childpid = fork();
if (childpid == 0) {
exit(execlp("bash", " bash",
@ -3335,7 +3818,6 @@ TEST_CASE("nvs find key tests", "[nvs]")
const uint32_t NVS_FLASH_SECTOR = 6;
const uint32_t NVS_FLASH_SECTOR_COUNT_MIN = 13;
TEST_ESP_ERR(nvs_open("namespace1", NVS_READWRITE, &handle_1), ESP_ERR_NVS_NOT_INITIALIZED);
for (uint16_t i = NVS_FLASH_SECTOR; i < NVS_FLASH_SECTOR + NVS_FLASH_SECTOR_COUNT_MIN; ++i) {
f.erase(i);
@ -3375,11 +3857,15 @@ TEST_CASE("nvs find key tests", "[nvs]")
uint8_t *p_buff = (uint8_t *) malloc(buff_len);
CHECK(p_buff != nullptr);
TEST_ESP_ERR(nvs_find_key(handle_1, "foo2", &datatype_found), ESP_ERR_NVS_NOT_FOUND);
for(size_t i=0; i<buff_len; i++) p_buff[i] = (uint8_t) (i%0xff);
for (size_t i = 0; i < buff_len; i++) {
p_buff[i] = (uint8_t)(i % 0xff);
}
TEST_ESP_OK(nvs_set_blob(handle_1, "foo2", p_buff, buff_len));
TEST_ESP_OK(nvs_find_key(handle_1, "foo2", &datatype_found));
CHECK(datatype_found == NVS_TYPE_BLOB);
for(size_t i=0; i<buff_len; i++) p_buff[i] = (uint8_t) ((buff_len-i-1)%0xff);
for (size_t i = 0; i < buff_len; i++) {
p_buff[i] = (uint8_t)((buff_len - i - 1) % 0xff);
}
TEST_ESP_OK(nvs_set_blob(handle_1, "foo2", p_buff, buff_len));
TEST_ESP_OK(nvs_find_key(handle_1, "foo2", &datatype_found));
CHECK(datatype_found == NVS_TYPE_BLOB);

View File

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@ -10,8 +10,7 @@
#include <cstring>
#include "nvs_internal.h"
namespace nvs
{
namespace nvs {
Page::Page() : mPartition(nullptr) { }
@ -46,7 +45,9 @@ esp_err_t Page::load(Partition *partition, uint32_t sectorNumber)
const int BLOCK_SIZE = 128;
uint32_t* block = new (std::nothrow) uint32_t[BLOCK_SIZE];
if (!block) return ESP_ERR_NO_MEM;
if (!block) {
return ESP_ERR_NO_MEM;
}
for (uint32_t i = 0; i < SPI_FLASH_SEC_SIZE; i += 4 * BLOCK_SIZE) {
rc = mPartition->read_raw(mBaseAddress + i, block, 4 * BLOCK_SIZE);
@ -101,7 +102,6 @@ esp_err_t Page::writeEntry(const Item& item)
}
err = mPartition->write(phyAddr, &item, sizeof(item));
if (err != ESP_OK) {
mState = PageState::INVALID;
return err;
@ -386,7 +386,7 @@ esp_err_t Page::eraseEntryAndSpan(size_t index)
if (rc != ESP_OK) {
return rc;
}
if (item.calculateCrc32() != item.crc32) {
if (!item.checkHeaderConsistency(index)) {
mHashList.erase(index);
rc = alterEntryState(index, EntryState::ERASED);
--mUsedEntryCount;
@ -508,7 +508,10 @@ esp_err_t Page::copyItems(Page& other)
NVS_ASSERT_OR_RETURN(end <= ENTRY_COUNT, ESP_FAIL);
for (size_t i = readEntryIndex + 1; i < end; ++i) {
readEntry(i, entry);
err = readEntry(i, entry);
if (err != ESP_OK) {
return err;
}
err = other.writeEntry(entry);
if (err != ESP_OK) {
return err;
@ -599,8 +602,7 @@ esp_err_t Page::mLoadEntryTable()
--mUsedEntryCount;
}
++mErasedEntryCount;
}
else {
} else {
break;
}
}
@ -642,7 +644,7 @@ esp_err_t Page::mLoadEntryTable()
return err;
}
if (item.crc32 != item.calculateCrc32()) {
if (!item.checkHeaderConsistency(i)) {
err = eraseEntryAndSpan(i);
if (err != ESP_OK) {
mState = PageState::INVALID;
@ -722,7 +724,7 @@ esp_err_t Page::mLoadEntryTable()
return err;
}
if (item.crc32 != item.calculateCrc32()) {
if (!item.checkHeaderConsistency(i)) {
err = eraseEntryAndSpan(i);
if (err != ESP_OK) {
mState = PageState::INVALID;
@ -762,7 +764,6 @@ esp_err_t Page::mLoadEntryTable()
return ESP_OK;
}
esp_err_t Page::initialize()
{
NVS_ASSERT_OR_RETURN(mState == PageState::UNINITIALIZED, ESP_FAIL);
@ -910,8 +911,7 @@ esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, si
return rc;
}
auto crc32 = item.calculateCrc32();
if (item.crc32 != crc32) {
if (!item.checkHeaderConsistency(i)) {
rc = eraseEntryAndSpan(i);
if (rc != ESP_OK) {
mState = PageState::INVALID;
@ -975,7 +975,6 @@ esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, si
continue;
}
if (datatype != ItemType::ANY && item.datatype != datatype) {
if (key == nullptr && nsIndex == NS_ANY && chunkIdx == CHUNK_ANY) {
continue; // continue for bruteforce search on blob indices.
@ -1001,7 +1000,6 @@ esp_err_t Page::getSeqNumber(uint32_t& seqNumber) const
return ESP_ERR_NVS_NOT_INITIALIZED;
}
esp_err_t Page::setSeqNumber(uint32_t seqNumber)
{
if (mState != PageState::UNINITIALIZED) {

View File

@ -1,22 +1,16 @@
// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
//
// 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.
/*
* SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "nvs_types.hpp"
#include "nvs_page.hpp"
#include "esp_log.h"
#include "esp_rom_crc.h"
namespace nvs
{
#define TAG "nvs"
namespace nvs {
uint32_t Item::calculateCrc32() const
{
uint32_t result = 0xffffffff;
@ -46,4 +40,114 @@ uint32_t Item::calculateCrc32(const uint8_t* data, size_t size)
return result;
}
bool Item::checkHeaderConsistency(const uint8_t entryIndex) const
{
// calculate and check the crc32
if (crc32 != calculateCrc32()) {
ESP_LOGD(TAG, "CRC32 mismatch for entry %d", entryIndex);
return false;
}
// validate the datatype and check the rest of the header fields
switch (datatype) {
// Entries occupying just one entry
case ItemType::U8:
case ItemType::I8:
case ItemType::U16:
case ItemType::I16:
case ItemType::U32:
case ItemType::I32:
case ItemType::U64:
case ItemType::I64: {
if (span != 1) {
ESP_LOGD(TAG, "Invalid span %u for datatype %#04x", (unsigned int)span, (unsigned int)datatype);
return false;
}
break;
}
// Special case for BLOB_IDX
case ItemType::BLOB_IDX: {
// span must be 1
if (span != 1) {
ESP_LOGD(TAG, "Invalid span %u for BLOB_IDX", (unsigned int)span);
return false;
}
// chunkIndex must be CHUNK_ANY
if (chunkIndex != CHUNK_ANY) {
ESP_LOGD(TAG, "Invalid chunk index %u for BLOB_IDX", (unsigned int)chunkIndex);
return false;
}
// check maximum data length
// the maximal data length is determined by:
// maximum number of chunks. Chunks are stored in uin8_t, but are logically divided into two "VerOffset" ranges of values (0 based and 128 based)
// maximum theoretical number of entries in the chunk (Page::ENTRY_COUNT - 1) and the number of bytes entry can store (Page::ENTRY_SIZE)
const uint32_t maxDataSize = (uint32_t)((UINT8_MAX / 2) * (Page::ENTRY_COUNT - 1) * Page::ENTRY_SIZE);
if (blobIndex.dataSize > maxDataSize) {
ESP_LOGD(TAG, "Data size %u bytes exceeds maximum possible size %u bytes for BLOB_IDX", (unsigned int)blobIndex.dataSize, (unsigned int)maxDataSize);
return false;
}
break;
}
// Entries with variable length data
case ItemType::SZ:
case ItemType::BLOB:
case ItemType::BLOB_DATA: {
uint16_t maxAvailableVDataSize;
uint8_t maxAvailablePageSpan;
uint8_t spanCalcFromLen;
// for BLOB_DATA, chunkIndex must NOT be CHUNK_ANY as this value is used to search ALL chunks in findItem
if (datatype == ItemType::BLOB_DATA) {
// chunkIndex must not be CHUNK_ANY
if (chunkIndex == CHUNK_ANY) {
ESP_LOGD(TAG, "Invalid chunk index %u for BLOB_DATA", (unsigned int)chunkIndex);
return false;
}
}
// variable length and span checks
// based on the entryIndex determine the maximum variable length capacity in bytes to the end of the page
maxAvailableVDataSize = ((Page::ENTRY_COUNT - entryIndex) - 1) * Page::ENTRY_SIZE;
// check if the variable data length is not exceeding the maximum capacity available till the end of the page
if (varLength.dataSize > maxAvailableVDataSize) {
ESP_LOGD(TAG, "Variable data length %u bytes exceeds page boundary. Maximum calculated from the current entry position within page is %u bytes for datatype %#04x ", (unsigned int)varLength.dataSize, (unsigned int)maxAvailableVDataSize, (unsigned int)datatype);
return false;
}
// based on the entryIndex determine the maximum possible span up to the end of the page
maxAvailablePageSpan = Page::ENTRY_COUNT - entryIndex;
// this check ensures no data is read beyond the end of the page
if (span > maxAvailablePageSpan) {
ESP_LOGD(TAG, "Span %u exceeds page boundary. Maximum calculated from the current entry position within page is %u for datatype %#04x ", (unsigned int)span, (unsigned int)maxAvailablePageSpan, (unsigned int)datatype);
return false;
}
// here we have both span and varLength.dataSize within the page boundary. Check if these values are consistent
spanCalcFromLen = (uint8_t)(((size_t) varLength.dataSize + Page::ENTRY_SIZE - 1) / Page::ENTRY_SIZE);
spanCalcFromLen ++; // add overhead entry
// this check ensures that the span is equal to the number of entries required to store the data plus the overhead entry
if (span != spanCalcFromLen) {
ESP_LOGD(TAG, "Span %i does not match span %u calculated from variable data length %u bytes for datatype %#04x", (unsigned int)span, (unsigned int)spanCalcFromLen, (unsigned int)varLength.dataSize, (unsigned int)datatype);
return false;
}
break;
}
// Invalid datatype
default: {
ESP_LOGD(TAG, "Invalid datatype %#04x", (unsigned int)datatype);
return false;
}
}
return true;
}
} // namespace nvs

View File

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@ -108,6 +108,14 @@ public:
dst = *reinterpret_cast<T*>(data);
return ESP_OK;
}
// Returns true if item's header:
// crc32 matches the calculated crc32
// and datatype is one of the supported types
// and span is within the allowed range for the datatype and below the maximum calculated from the entryIndex
//
// Parameter entryIndex is used to calculate the maximum span for the given entry
bool checkHeaderConsistency(const uint8_t entryIndex) const;
};
} // namespace nvs

View File

@ -499,7 +499,6 @@ components/nvs_flash/src/nvs_pagemanager.hpp
components/nvs_flash/src/nvs_partition_lookup.cpp
components/nvs_flash/src/nvs_partition_lookup.hpp
components/nvs_flash/src/nvs_test_api.h
components/nvs_flash/src/nvs_types.cpp
components/nvs_flash/test_nvs_host/main.cpp
components/nvs_flash/test_nvs_host/sdkconfig.h
components/nvs_flash/test_nvs_host/test_intrusive_list.cpp