From 508c251fd22b6e9e4eec22413306ac1031eb91cc Mon Sep 17 00:00:00 2001 From: "radek.tandler" Date: Mon, 29 Jul 2024 13:50:12 +0200 Subject: [PATCH] fix(storage/nvs): Fixed hadling of inconsistent values in NVS entry header feat(storage/nvs): Added test cases for damaged entries with correct CRC --- components/nvs_flash/src/nvs_page.cpp | 134 +++++++++++------------ components/nvs_flash/src/nvs_types.cpp | 140 +++++++++++++++++++++---- components/nvs_flash/src/nvs_types.hpp | 10 +- tools/ci/check_copyright_ignore.txt | 1 - 4 files changed, 198 insertions(+), 87 deletions(-) diff --git a/components/nvs_flash/src/nvs_page.cpp b/components/nvs_flash/src/nvs_page.cpp index ad3b1ce1e2..ddd56c201a 100644 --- a/components/nvs_flash/src/nvs_page.cpp +++ b/components/nvs_flash/src/nvs_page.cpp @@ -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 */ @@ -9,16 +9,15 @@ #include #include "nvs_internal.h" -namespace nvs -{ +namespace nvs { Page::Page() : mPartition(nullptr) { } uint32_t Page::Header::calculateCrc32() { return esp_rom_crc32_le(0xffffffff, - reinterpret_cast(this) + offsetof(Header, mSeqNumber), - offsetof(Header, mCrc32) - offsetof(Header, mSeqNumber)); + reinterpret_cast(this) + offsetof(Header, mSeqNumber), + offsetof(Header, mCrc32) - offsetof(Header, mSeqNumber)); } esp_err_t Page::load(Partition *partition, uint32_t sectorNumber) @@ -45,7 +44,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); @@ -66,7 +67,7 @@ esp_err_t Page::load(Partition *partition, uint32_t sectorNumber) } else { mState = header.mState; mSeqNumber = header.mSeqNumber; - if(header.mVersion < NVS_VERSION) { + if (header.mVersion < NVS_VERSION) { return ESP_ERR_NVS_NEW_VERSION_FOUND; } else { mVersion = header.mVersion; @@ -91,7 +92,7 @@ esp_err_t Page::load(Partition *partition, uint32_t sectorNumber) return ESP_OK; } -esp_err_t Page::writeEntry(const Item& item) +esp_err_t Page::writeEntry(const Item &item) { uint32_t phyAddr; esp_err_t err = getEntryAddress(mNextFreeEntry, &phyAddr); @@ -100,7 +101,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; @@ -215,7 +215,7 @@ esp_err_t Page::writeItem(uint8_t nsIndex, ItemType datatype, const char* key, c // primitive types should fit into one entry NVS_ASSERT_OR_RETURN(totalSize == ENTRY_SIZE || - isVariableLengthType(datatype), ESP_ERR_NVS_VALUE_TOO_LONG); + isVariableLengthType(datatype), ESP_ERR_NVS_VALUE_TOO_LONG); if (mNextFreeEntry == INVALID_ENTRY || mNextFreeEntry + entriesCount > ENTRY_COUNT) { // page will not fit this amount of data @@ -308,12 +308,12 @@ esp_err_t Page::readItem(uint8_t nsIndex, ItemType datatype, const char* key, vo return rc; } size_t willCopy = ENTRY_SIZE; - willCopy = (left < willCopy)?left:willCopy; + willCopy = (left < willCopy) ? left : willCopy; memcpy(dst, ditem.rawData, willCopy); left -= willCopy; dst += willCopy; } - if (Item::calculateCrc32(reinterpret_cast(data), item.varLength.dataSize) != item.varLength.dataCrc32) { + if (Item::calculateCrc32(reinterpret_cast(data), item.varLength.dataSize) != item.varLength.dataCrc32) { rc = eraseEntryAndSpan(index); if (rc != ESP_OK) { return rc; @@ -361,14 +361,14 @@ esp_err_t Page::cmpItem(uint8_t nsIndex, ItemType datatype, const char* key, con return rc; } size_t willCopy = ENTRY_SIZE; - willCopy = (left < willCopy)?left:willCopy; + willCopy = (left < willCopy) ? left : willCopy; if (memcmp(dst, ditem.rawData, willCopy)) { return ESP_ERR_NVS_CONTENT_DIFFERS; } left -= willCopy; dst += willCopy; } - if (Item::calculateCrc32(reinterpret_cast(data), item.varLength.dataSize) != item.varLength.dataCrc32) { + if (Item::calculateCrc32(reinterpret_cast(data), item.varLength.dataSize) != item.varLength.dataCrc32) { return ESP_ERR_NVS_NOT_FOUND; } @@ -411,7 +411,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; @@ -485,7 +485,7 @@ esp_err_t Page::updateFirstUsedEntry(size_t index, size_t span) return ESP_OK; } -esp_err_t Page::copyItems(Page& other) +esp_err_t Page::copyItems(Page &other) { if (mFirstUsedEntry == INVALID_ENTRY) { return ESP_ERR_NVS_NOT_FOUND; @@ -533,7 +533,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; @@ -552,7 +555,7 @@ esp_err_t Page::mLoadEntryTable() mState == PageState::FULL || mState == PageState::FREEING) { auto rc = mPartition->read_raw(mBaseAddress + ENTRY_TABLE_OFFSET, mEntryTable.data(), - mEntryTable.byteSize()); + mEntryTable.byteSize()); if (rc != ESP_OK) { mState = PageState::INVALID; return rc; @@ -624,8 +627,7 @@ esp_err_t Page::mLoadEntryTable() --mUsedEntryCount; } ++mErasedEntryCount; - } - else { + } else { break; } } @@ -667,7 +669,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; @@ -747,7 +749,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; @@ -787,7 +789,6 @@ esp_err_t Page::mLoadEntryTable() return ESP_OK; } - esp_err_t Page::initialize() { NVS_ASSERT_OR_RETURN(mState == PageState::UNINITIALIZED, ESP_FAIL); @@ -819,7 +820,7 @@ esp_err_t Page::alterEntryState(size_t index, EntryState state) size_t wordToWrite = mEntryTable.getWordIndex(index); uint32_t word = mEntryTable.data()[wordToWrite]; err = mPartition->write_raw(mBaseAddress + ENTRY_TABLE_OFFSET + static_cast(wordToWrite) * 4, - &word, sizeof(word)); + &word, sizeof(word)); if (err != ESP_OK) { mState = PageState::INVALID; return err; @@ -835,7 +836,7 @@ esp_err_t Page::alterEntryRangeState(size_t begin, size_t end, EntryState state) esp_err_t err; for (ptrdiff_t i = end - 1; i >= static_cast(begin); --i) { err = mEntryTable.set(i, state); - if (err != ESP_OK){ + if (err != ESP_OK) { return err; } size_t nextWordIndex; @@ -847,7 +848,7 @@ esp_err_t Page::alterEntryRangeState(size_t begin, size_t end, EntryState state) if (nextWordIndex != wordIndex) { uint32_t word = mEntryTable.data()[wordIndex]; auto rc = mPartition->write_raw(mBaseAddress + ENTRY_TABLE_OFFSET + static_cast(wordIndex) * 4, - &word, 4); + &word, 4); if (rc != ESP_OK) { return rc; } @@ -869,7 +870,7 @@ esp_err_t Page::alterPageState(PageState state) return ESP_OK; } -esp_err_t Page::readEntry(size_t index, Item& dst) const +esp_err_t Page::readEntry(size_t index, Item &dst) const { uint32_t phyAddr; esp_err_t rc = getEntryAddress(index, &phyAddr); @@ -883,7 +884,7 @@ esp_err_t Page::readEntry(size_t index, Item& dst) const return ESP_OK; } -esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, size_t &itemIndex, Item& item, uint8_t chunkIdx, VerOffset chunkStart) +esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, size_t &itemIndex, Item &item, uint8_t chunkIdx, VerOffset chunkStart) { if (mState == PageState::CORRUPT || mState == PageState::INVALID || mState == PageState::UNINITIALIZED) { return ESP_ERR_NVS_NOT_FOUND; @@ -935,8 +936,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; @@ -1000,7 +1000,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. @@ -1017,7 +1016,7 @@ esp_err_t Page::findItem(uint8_t nsIndex, ItemType datatype, const char* key, si return ESP_ERR_NVS_NOT_FOUND; } -esp_err_t Page::getSeqNumber(uint32_t& seqNumber) const +esp_err_t Page::getSeqNumber(uint32_t &seqNumber) const { if (mState != PageState::UNINITIALIZED && mState != PageState::INVALID && mState != PageState::CORRUPT) { seqNumber = mSeqNumber; @@ -1026,7 +1025,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) { @@ -1084,40 +1082,41 @@ size_t Page::getVarDataTailroom() const } else if (mState == PageState::FULL) { return 0; } - /* Skip one entry for blob data item precessing the data */ - return ((mNextFreeEntry < (ENTRY_COUNT-1)) ? ((ENTRY_COUNT - mNextFreeEntry - 1) * ENTRY_SIZE): 0); + /* Skip one entry for blob data item processing the data */ + return ((mNextFreeEntry < (ENTRY_COUNT - 1)) ? ((ENTRY_COUNT - mNextFreeEntry - 1) * ENTRY_SIZE) : 0); } const char* Page::pageStateToName(PageState ps) { switch (ps) { - case PageState::CORRUPT: - return "CORRUPT"; + case PageState::CORRUPT: + return "CORRUPT"; - case PageState::ACTIVE: - return "ACTIVE"; + case PageState::ACTIVE: + return "ACTIVE"; - case PageState::FREEING: - return "FREEING"; + case PageState::FREEING: + return "FREEING"; - case PageState::FULL: - return "FULL"; + case PageState::FULL: + return "FULL"; - case PageState::INVALID: - return "INVALID"; + case PageState::INVALID: + return "INVALID"; - case PageState::UNINITIALIZED: - return "UNINITIALIZED"; + case PageState::UNINITIALIZED: + return "UNINITIALIZED"; - default: - assert(0 && "invalid state value"); - return ""; + default: + assert(0 && "invalid state value"); + return ""; } } void Page::debugDump() const { - printf("state=%x (%s) addr=%x seq=%d\nfirstUsed=%d nextFree=%d used=%d erased=%d\n", (uint32_t) mState, pageStateToName(mState), mBaseAddress, mSeqNumber, static_cast(mFirstUsedEntry), static_cast(mNextFreeEntry), mUsedEntryCount, mErasedEntryCount); + printf("state=%x (%s) addr=%x seq=%d\nfirstUsed=%d nextFree=%d used=%d erased=%d\n", + (uint32_t) mState, pageStateToName(mState), mBaseAddress, mSeqNumber, static_cast(mFirstUsedEntry), static_cast(mNextFreeEntry), mUsedEntryCount, mErasedEntryCount); size_t skip = 0; for (size_t i = 0; i < ENTRY_COUNT; ++i) { printf("%3d: ", static_cast(i)); @@ -1134,7 +1133,8 @@ void Page::debugDump() const Item item; readEntry(i, item); if (skip == 0) { - printf("W ns=%2u type=%2u span=%3u key=\"%s\" chunkIdx=%d len=%d\n", item.nsIndex, static_cast(item.datatype), item.span, item.key, item.chunkIndex, (item.span != 1)?((int)item.varLength.dataSize):-1); + printf("W ns=%2u type=%2u span=%3u key=\"%s\" chunkIdx=%d len=%d\n", + item.nsIndex, static_cast(item.datatype), item.span, item.key, item.chunkIndex, (item.span != 1)?((int)item.varLength.dataSize):-1); if (item.span > 0 && item.span <= ENTRY_COUNT - i) { skip = item.span - 1; } else { @@ -1155,24 +1155,24 @@ esp_err_t Page::calcEntries(nvs_stats_t &nvsStats) nvsStats.total_entries += ENTRY_COUNT; switch (mState) { - case PageState::UNINITIALIZED: - case PageState::CORRUPT: - nvsStats.free_entries += ENTRY_COUNT; - break; + case PageState::UNINITIALIZED: + case PageState::CORRUPT: + nvsStats.free_entries += ENTRY_COUNT; + break; - case PageState::FULL: - case PageState::ACTIVE: - nvsStats.used_entries += mUsedEntryCount; - nvsStats.free_entries += ENTRY_COUNT - mUsedEntryCount; // it's equivalent free + erase entries. - break; + case PageState::FULL: + case PageState::ACTIVE: + nvsStats.used_entries += mUsedEntryCount; + nvsStats.free_entries += ENTRY_COUNT - mUsedEntryCount; // it's equivalent free + erase entries. + break; - case PageState::INVALID: - return ESP_ERR_INVALID_STATE; - break; + case PageState::INVALID: + return ESP_ERR_INVALID_STATE; + break; - default: - assert(false && "Unhandled state"); - break; + default: + assert(false && "Unhandled state"); + break; } return ESP_OK; } diff --git a/components/nvs_flash/src/nvs_types.cpp b/components/nvs_flash/src/nvs_types.cpp index a9369a4e27..24073fc306 100644 --- a/components/nvs_flash/src/nvs_types.cpp +++ b/components/nvs_flash/src/nvs_types.cpp @@ -1,28 +1,22 @@ -// 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; const uint8_t* p = reinterpret_cast(this); result = esp_rom_crc32_le(result, p + offsetof(Item, nsIndex), - offsetof(Item, crc32) - offsetof(Item, nsIndex)); + offsetof(Item, crc32) - offsetof(Item, nsIndex)); result = esp_rom_crc32_le(result, p + offsetof(Item, key), sizeof(key)); result = esp_rom_crc32_le(result, p + offsetof(Item, data), sizeof(data)); return result; @@ -33,7 +27,7 @@ uint32_t Item::calculateCrc32WithoutValue() const uint32_t result = 0xffffffff; const uint8_t* p = reinterpret_cast(this); result = esp_rom_crc32_le(result, p + offsetof(Item, nsIndex), - offsetof(Item, datatype) - offsetof(Item, nsIndex)); + offsetof(Item, datatype) - offsetof(Item, nsIndex)); result = esp_rom_crc32_le(result, p + offsetof(Item, key), sizeof(key)); result = esp_rom_crc32_le(result, p + offsetof(Item, chunkIndex), sizeof(chunkIndex)); return result; @@ -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 diff --git a/components/nvs_flash/src/nvs_types.hpp b/components/nvs_flash/src/nvs_types.hpp index 53d3555693..5eda9abcd9 100644 --- a/components/nvs_flash/src/nvs_types.hpp +++ b/components/nvs_flash/src/nvs_types.hpp @@ -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(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 diff --git a/tools/ci/check_copyright_ignore.txt b/tools/ci/check_copyright_ignore.txt index 6df24bac44..29000d27ab 100644 --- a/tools/ci/check_copyright_ignore.txt +++ b/tools/ci/check_copyright_ignore.txt @@ -835,7 +835,6 @@ components/nvs_flash/src/nvs_partition.cpp 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/src/partition.hpp components/nvs_flash/test/test_nvs.c components/nvs_flash/test_nvs_host/esp_error_check_stub.cpp