mirror of
https://github.com/Telecominfraproject/OpenCellular.git
synced 2026-01-04 05:51:34 +00:00
If both nvmem partitions are corrupted, then nvmem_reinitialize() will be called and commits must be enabled prior to this so that nvmem_release_cache() succeeds. BRANCH=none BUG=chrome-os-partner:62531 TEST=On Reef after a successful boot, corrupt nvmem partitions using the following console commands: flasherase 0x7d000 0x3000 flasherase 0x3d000 0x3000 Then reboot via H1 console and verified via the console that the nvmem partitions were reconfigured. nvmem_find_partition:302 partiton 0 verification FAILED nvmem_find_partition:302 partiton 1 verification FAILED [0.025928 nvmem_find_partition: No Valid Partition found, will reinitialize!] [0.127752 Active Nvmem partition set to 1] Then verfied that TPM was functional and the system booted booted into the kernel. Without this CL this set of actions would always result in going in to recovery mode due to TPM failure. Change-Id: If1691b179e19cb37f0fc6ba893219dd8c02f2cf5 Signed-off-by: Scott <scollyer@chromium.org> Reviewed-on: https://chromium-review.googlesource.com/439368 Commit-Ready: Scott Collyer <scollyer@chromium.org> Tested-by: Scott Collyer <scollyer@chromium.org> Reviewed-by: Aaron Durbin <adurbin@chromium.org>
598 lines
16 KiB
C
598 lines
16 KiB
C
/* Copyright 2016 The Chromium OS Authors. All rights reserved.
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "console.h"
|
|
#include "flash.h"
|
|
#include "nvmem.h"
|
|
#include "task.h"
|
|
#include "timer.h"
|
|
#include "util.h"
|
|
|
|
#define CPRINTF(format, args...) cprintf(CC_COMMAND, format, ## args)
|
|
#define CPRINTS(format, args...) cprints(CC_COMMAND, format, ## args)
|
|
|
|
#define NVMEM_NOT_INITIALIZED (-1)
|
|
|
|
/*
|
|
* The NVMEM contents are stored in flash memory. At run time there is an SRAM
|
|
* cache and two instances of the contents in the flash in two partitions.
|
|
*
|
|
* Each instance is protected by a 16 bytes hash and has a 'generation' value
|
|
* associated with it. When NVMEM module is initialized it checks the flash
|
|
* stored instances. If both of them are valid, it considers the newer one
|
|
* (younger generation) to be the proper NVMEM contents and copies it to the
|
|
* SRAM cache. If only one instance is valid, it is used, and if no instances
|
|
* are valid - a new valid partition is created and copied into the SRAM
|
|
* cache.
|
|
*
|
|
* When stored in flash, the contents are encrypted, the hash value is used as
|
|
* the IV for the encryption routine.
|
|
*
|
|
* There is a mutex controlling access to the NVMEM. There are two levels
|
|
* of protection - for read only accesses and for write accesses. When the
|
|
* module is initialized the mutex is opened.
|
|
*
|
|
* If there are no pending writes, each read access locks the mutex, reads out
|
|
* the data and unlocks the mutex, thus multiple tasks could be reading NVMEM,
|
|
* blocking access momentarily.
|
|
*
|
|
* If a write access ever occurs things get more complicated. The write access
|
|
* leaves the mutex locked and stores the flag, indicating that the
|
|
* contents have changed and need to be saved, and stores the task id of the
|
|
* task performing the write access.
|
|
*
|
|
* The mutex remains locked in this case. Next time a read access happens,
|
|
* if it comes from the same task, the unlock in the end of the read is
|
|
* bypassed because the 'write in progress' flag is set. If a read or write
|
|
* request comes from another task, they will be blocked until the first
|
|
* task to write commits.
|
|
*
|
|
* nvmem_commit() calls the nvmem_save() function which checks if the cache
|
|
* contents indeed changed (by calculating the hash again). If there is no
|
|
* change - the mutex is released and the function exits. If there is a
|
|
* change, the new generation value is set, the new hash is calculated
|
|
* and the copy is saved in the least recently used flash partition, and
|
|
* then the lock is released.
|
|
*/
|
|
|
|
/* Table of start addresses for each partition */
|
|
static const uintptr_t nvmem_base_addr[NVMEM_NUM_PARTITIONS] = {
|
|
CONFIG_FLASH_NVMEM_BASE_A,
|
|
CONFIG_FLASH_NVMEM_BASE_B
|
|
};
|
|
|
|
/* NvMem user buffer start offset table */
|
|
static uint32_t nvmem_user_start_offset[NVMEM_NUM_USERS];
|
|
|
|
/* A/B partion that is most up to date */
|
|
static int nvmem_act_partition;
|
|
|
|
/* NvMem cache memory structure */
|
|
struct nvmem_mutex_ {
|
|
task_id_t task;
|
|
int write_in_progress;
|
|
struct mutex mtx;
|
|
};
|
|
|
|
static struct nvmem_mutex_ nvmem_mutex = { .task = TASK_ID_COUNT };
|
|
static uint8_t nvmem_cache[NVMEM_PARTITION_SIZE] __aligned(4);
|
|
|
|
static uint8_t commits_enabled;
|
|
|
|
/* NvMem error state */
|
|
static int nvmem_error_state;
|
|
/* Flag to track if an Nv write/move is not completed */
|
|
static int nvmem_write_error;
|
|
|
|
static void nvmem_release_cache(void);
|
|
|
|
/*
|
|
* Given the nvmem tag address calculate the sha value of the nvmem buffer and
|
|
* save it in the provided space. The caller is expected to provide enough
|
|
* space to store CIPHER_SALT_SIZE bytes.
|
|
*/
|
|
static void nvmem_compute_sha(struct nvmem_tag *tag, void *sha_buf)
|
|
{
|
|
app_compute_hash(tag->padding, NVMEM_PARTITION_SIZE - NVMEM_SHA_SIZE,
|
|
sha_buf, sizeof(tag->sha));
|
|
}
|
|
|
|
static int nvmem_save(void)
|
|
{
|
|
struct nvmem_partition *part;
|
|
size_t nvmem_offset;
|
|
int dest_partition;
|
|
uint8_t sha_comp[NVMEM_SHA_SIZE];
|
|
int rv = EC_SUCCESS;
|
|
|
|
part = (struct nvmem_partition *)nvmem_cache;
|
|
|
|
/* Has anything changed in the cache? */
|
|
nvmem_compute_sha(&part->tag, sha_comp);
|
|
|
|
if (!memcmp(part->tag.sha, sha_comp, sizeof(part->tag.sha))) {
|
|
CPRINTF("%s: Nothing changed, skipping flash write\n",
|
|
__func__);
|
|
goto release_cache;
|
|
}
|
|
|
|
/* Get flash offset of the partition to save to. */
|
|
dest_partition = (nvmem_act_partition + 1) % NVMEM_NUM_PARTITIONS;
|
|
nvmem_offset = nvmem_base_addr[dest_partition] -
|
|
CONFIG_PROGRAM_MEMORY_BASE;
|
|
|
|
/* Erase partition */
|
|
rv = flash_physical_erase(nvmem_offset, NVMEM_PARTITION_SIZE);
|
|
if (rv != EC_SUCCESS) {
|
|
CPRINTF("%s flash erase failed\n", __func__);
|
|
goto release_cache;
|
|
}
|
|
|
|
part->tag.layout_version = NVMEM_LAYOUT_VERSION;
|
|
part->tag.generation++;
|
|
|
|
/* Calculate sha of the whole thing. */
|
|
nvmem_compute_sha(&part->tag, part->tag.sha);
|
|
|
|
/* Encrypt actual payload. */
|
|
if (!app_cipher(part->tag.sha, part->buffer, part->buffer,
|
|
sizeof(part->buffer))) {
|
|
CPRINTF("%s encryption failed\n", __func__);
|
|
rv = EC_ERROR_UNKNOWN;
|
|
goto release_cache;
|
|
}
|
|
|
|
rv = flash_physical_write(nvmem_offset,
|
|
NVMEM_PARTITION_SIZE,
|
|
nvmem_cache);
|
|
if (rv != EC_SUCCESS) {
|
|
CPRINTF("%s flash write failed\n", __func__);
|
|
goto release_cache;
|
|
}
|
|
|
|
/* Restore payload. */
|
|
if (!app_cipher(part->tag.sha, part->buffer, part->buffer,
|
|
sizeof(part->buffer))) {
|
|
CPRINTF("%s decryption failed\n", __func__);
|
|
rv = EC_ERROR_UNKNOWN;
|
|
goto release_cache;
|
|
}
|
|
|
|
nvmem_act_partition = dest_partition;
|
|
|
|
release_cache:
|
|
nvmem_mutex.write_in_progress = 0;
|
|
nvmem_release_cache();
|
|
return rv;
|
|
}
|
|
|
|
/*
|
|
* Read from flash and verify partition.
|
|
*
|
|
* @param index - index of the partition to verify
|
|
*
|
|
* Returns EC_SUCCESS on verification success
|
|
* EC_ERROR_BUSY in case of malloc failure
|
|
* EC_ERROR_UNKNOWN on failure to decrypt of verify.
|
|
*/
|
|
static int nvmem_partition_read_verify(int index)
|
|
{
|
|
uint8_t sha_comp[NVMEM_SHA_SIZE];
|
|
struct nvmem_partition *p_part;
|
|
struct nvmem_partition *p_copy;
|
|
int ret;
|
|
|
|
p_part = (struct nvmem_partition *)nvmem_base_addr[index];
|
|
p_copy = (struct nvmem_partition *)nvmem_cache;
|
|
memcpy(p_copy, p_part, NVMEM_PARTITION_SIZE);
|
|
|
|
/* Then decrypt it. */
|
|
if (!app_cipher(p_copy->tag.sha, &p_copy->tag + 1,
|
|
&p_copy->tag + 1,
|
|
NVMEM_PARTITION_SIZE - sizeof(struct nvmem_tag))) {
|
|
CPRINTF("%s: decryption failure\n", __func__);
|
|
return EC_ERROR_UNKNOWN;
|
|
}
|
|
|
|
/*
|
|
* Check if computed value matches stored value. Nonzero 'ret' value
|
|
* means there was a match.
|
|
*/
|
|
nvmem_compute_sha(&p_copy->tag, sha_comp);
|
|
ret = !memcmp(p_copy->tag.sha, sha_comp, NVMEM_SHA_SIZE);
|
|
|
|
return ret ? EC_SUCCESS : EC_ERROR_UNKNOWN;
|
|
}
|
|
|
|
static void nvmem_lock_cache(void)
|
|
{
|
|
/*
|
|
* Need to protect the cache contents value from other tasks
|
|
* attempting to do nvmem write operations. However, since this
|
|
* function may be called mutliple times prior to the mutex lock being
|
|
* released, there is a check first to see if the current task holds
|
|
* the lock. If it does then the task number will equal the value in
|
|
* cache.task, no need to wait.
|
|
*
|
|
* If the lock is held by a different task then mutex_lock function
|
|
* will operate as normal.
|
|
*/
|
|
if (nvmem_mutex.task == task_get_current())
|
|
return;
|
|
|
|
mutex_lock(&nvmem_mutex.mtx);
|
|
nvmem_mutex.task = task_get_current();
|
|
}
|
|
|
|
static void nvmem_release_cache(void)
|
|
{
|
|
if (nvmem_mutex.write_in_progress || !commits_enabled)
|
|
return; /* It will have to be saved first. */
|
|
|
|
/* Reset task number to max value */
|
|
nvmem_mutex.task = TASK_ID_COUNT;
|
|
/* Release mutex lock here */
|
|
mutex_unlock(&nvmem_mutex.mtx);
|
|
}
|
|
|
|
static int nvmem_reinitialize(void)
|
|
{
|
|
nvmem_lock_cache(); /* Unlocked by nvmem_save() below. */
|
|
/*
|
|
* NvMem is not properly initialized. Let's just erase everything and
|
|
* start over, so that at least 1 partition is ready to be used.
|
|
*/
|
|
nvmem_act_partition = 0;
|
|
|
|
memset(nvmem_cache, 0xff, NVMEM_PARTITION_SIZE);
|
|
|
|
/* Start with generation zero in the current active partition. */
|
|
return nvmem_save();
|
|
}
|
|
|
|
static int nvmem_compare_generation(void)
|
|
{
|
|
struct nvmem_partition *p_part;
|
|
uint16_t ver0, ver1;
|
|
uint32_t delta;
|
|
|
|
p_part = (struct nvmem_partition *)nvmem_base_addr[0];
|
|
ver0 = p_part->tag.generation;
|
|
p_part = (struct nvmem_partition *)nvmem_base_addr[1];
|
|
ver1 = p_part->tag.generation;
|
|
|
|
/* Compute generation difference accounting for wrap condition */
|
|
delta = (ver0 - ver1 + (1<<NVMEM_GENERATION_BITS)) &
|
|
NVMEM_GENERATION_MASK;
|
|
/*
|
|
* If generation number delta is positive in a circular sense then
|
|
* partition 0 has the newest generation number. Otherwise, it's
|
|
* partition 1.
|
|
*/
|
|
return delta < (1<<(NVMEM_GENERATION_BITS-1)) ? 0 : 1;
|
|
}
|
|
|
|
static int nvmem_find_partition(void)
|
|
{
|
|
int n;
|
|
|
|
/* Don't know which partition to use yet */
|
|
nvmem_act_partition = NVMEM_NOT_INITIALIZED;
|
|
/*
|
|
* Check each partition to determine if the sha is good. If both
|
|
* partitions have valid sha(s), then compare generation numbers to
|
|
* select the most recent one.
|
|
*/
|
|
for (n = 0; n < NVMEM_NUM_PARTITIONS; n++)
|
|
if (nvmem_partition_read_verify(n) == EC_SUCCESS) {
|
|
if (nvmem_act_partition == NVMEM_NOT_INITIALIZED)
|
|
nvmem_act_partition = n;
|
|
else
|
|
nvmem_act_partition =
|
|
nvmem_compare_generation();
|
|
} else {
|
|
ccprintf("%s:%d partiton %d verification FAILED\n",
|
|
__func__, __LINE__, n);
|
|
}
|
|
|
|
if (nvmem_act_partition != NVMEM_NOT_INITIALIZED)
|
|
return EC_SUCCESS;
|
|
|
|
/*
|
|
* If active_partition is still not selected, then neither partition
|
|
* is valid. Let's reinitialize the NVMEM - there is nothing else we
|
|
* can do.
|
|
*/
|
|
CPRINTS("%s: No Valid Partition found, will reinitialize!", __func__);
|
|
|
|
if (nvmem_reinitialize() != EC_SUCCESS) {
|
|
CPRINTS("%s: Reinitialization failed!!");
|
|
return EC_ERROR_UNKNOWN;
|
|
}
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
static int nvmem_generate_offset_table(void)
|
|
{
|
|
int n;
|
|
uint32_t start_offset;
|
|
|
|
/*
|
|
* Create table of starting offsets within partition for each user
|
|
* buffer that's been defined.
|
|
*/
|
|
start_offset = sizeof(struct nvmem_tag);
|
|
for (n = 0; n < NVMEM_NUM_USERS; n++) {
|
|
nvmem_user_start_offset[n] = start_offset;
|
|
start_offset += nvmem_user_sizes[n];
|
|
}
|
|
/* Verify that all defined user buffers fit within the partition */
|
|
if (start_offset > NVMEM_PARTITION_SIZE)
|
|
return EC_ERROR_OVERFLOW;
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
static int nvmem_get_partition_off(int user, uint32_t offset,
|
|
uint32_t len, uint32_t *p_buf_offset)
|
|
{
|
|
uint32_t start_offset;
|
|
|
|
/* Sanity check for user */
|
|
if (user >= NVMEM_NUM_USERS)
|
|
return EC_ERROR_OVERFLOW;
|
|
|
|
/* Get offset within the partition for the start of user buffer */
|
|
start_offset = nvmem_user_start_offset[user];
|
|
/*
|
|
* Ensure that read/write operation that is calling this function
|
|
* doesn't exceed the end of its buffer.
|
|
*/
|
|
if (offset + len > nvmem_user_sizes[user])
|
|
return EC_ERROR_OVERFLOW;
|
|
/* Compute offset within the partition for the rd/wr operation */
|
|
*p_buf_offset = start_offset + offset;
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_setup(void)
|
|
{
|
|
int part;
|
|
int ret;
|
|
|
|
CPRINTS("Configuring NVMEM Flash Partition");
|
|
|
|
part = nvmem_act_partition;
|
|
nvmem_act_partition = 0;
|
|
|
|
ret = EC_SUCCESS;
|
|
|
|
for (part = 0; part < NVMEM_NUM_PARTITIONS; part++) {
|
|
int rv;
|
|
|
|
memset(nvmem_cache, 0xff, NVMEM_PARTITION_SIZE);
|
|
/*
|
|
* Make sure the contents change between runs of
|
|
* nvmem_save() so that both flash partitions are
|
|
* written with empty contents and different
|
|
* generation numbers.
|
|
*/
|
|
((struct nvmem_partition *)nvmem_cache)->tag.generation = part;
|
|
/* Lock the cache buffer (unlocked by nvmem_save() */
|
|
nvmem_lock_cache();
|
|
rv = nvmem_save();
|
|
|
|
/* Even if one partition saving failed, let's keep going. */
|
|
if (rv != EC_SUCCESS)
|
|
ret = rv;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
int nvmem_init(void)
|
|
{
|
|
int ret;
|
|
|
|
/* Generate start offsets within partiion for user buffers */
|
|
ret = nvmem_generate_offset_table();
|
|
if (ret) {
|
|
CPRINTF("%s:%d\n", __func__, __LINE__);
|
|
return ret;
|
|
}
|
|
/* Initialize error state, assume everything is good */
|
|
nvmem_error_state = EC_SUCCESS;
|
|
nvmem_write_error = 0;
|
|
|
|
/*
|
|
* Default policy is to allow all commits. This ensures reinitialization
|
|
* succeeds to bootstrap the nvmem area.
|
|
*/
|
|
commits_enabled = 1;
|
|
ret = nvmem_find_partition();
|
|
|
|
if (ret != EC_SUCCESS) {
|
|
/* Change error state to non-zero */
|
|
nvmem_error_state = ret;
|
|
CPRINTF("%s:%d\n", __func__, __LINE__);
|
|
return ret;
|
|
}
|
|
|
|
CPRINTS("Active Nvmem partition set to %d", nvmem_act_partition);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_get_error_state(void)
|
|
{
|
|
return nvmem_error_state;
|
|
}
|
|
|
|
int nvmem_is_different(uint32_t offset, uint32_t size, void *data,
|
|
enum nvmem_users user)
|
|
{
|
|
int ret;
|
|
uint32_t src_offset;
|
|
|
|
nvmem_lock_cache();
|
|
|
|
/* Get partition offset for this read operation */
|
|
ret = nvmem_get_partition_off(user, offset, size, &src_offset);
|
|
if (ret != EC_SUCCESS)
|
|
return ret;
|
|
|
|
/* Advance to the correct byte within the data buffer */
|
|
|
|
/* Compare NvMem with data */
|
|
ret = memcmp(nvmem_cache + src_offset, data, size);
|
|
|
|
nvmem_release_cache();
|
|
|
|
return ret;
|
|
}
|
|
|
|
int nvmem_read(uint32_t offset, uint32_t size,
|
|
void *data, enum nvmem_users user)
|
|
{
|
|
int ret;
|
|
uint32_t src_offset;
|
|
|
|
nvmem_lock_cache();
|
|
|
|
/* Get partition offset for this read operation */
|
|
ret = nvmem_get_partition_off(user, offset, size, &src_offset);
|
|
|
|
if (ret == EC_SUCCESS)
|
|
/* Copy from src into the caller's destination buffer */
|
|
memcpy(data, nvmem_cache + src_offset, size);
|
|
|
|
nvmem_release_cache();
|
|
|
|
return ret;
|
|
}
|
|
|
|
int nvmem_write(uint32_t offset, uint32_t size,
|
|
void *data, enum nvmem_users user)
|
|
{
|
|
int ret;
|
|
uint8_t *p_dest;
|
|
uint32_t dest_offset;
|
|
|
|
/* Make sure that the cache buffer is active */
|
|
nvmem_lock_cache();
|
|
nvmem_mutex.write_in_progress = 1;
|
|
|
|
/* Compute partition offset for this write operation */
|
|
ret = nvmem_get_partition_off(user, offset, size, &dest_offset);
|
|
if (ret != EC_SUCCESS) {
|
|
nvmem_write_error = 1;
|
|
return ret;
|
|
}
|
|
|
|
/* Advance to correct offset within data buffer */
|
|
p_dest = nvmem_cache + dest_offset;
|
|
|
|
/* Copy data from caller into destination buffer */
|
|
memcpy(p_dest, data, size);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_move(uint32_t src_offset, uint32_t dest_offset, uint32_t size,
|
|
enum nvmem_users user)
|
|
{
|
|
int ret;
|
|
uint8_t *p_src, *p_dest;
|
|
uintptr_t base_addr;
|
|
uint32_t s_buff_offset, d_buff_offset;
|
|
|
|
/* Make sure that the cache buffer is active */
|
|
nvmem_lock_cache();
|
|
nvmem_mutex.write_in_progress = 1;
|
|
|
|
/* Compute partition offset for source */
|
|
ret = nvmem_get_partition_off(user, src_offset, size, &s_buff_offset);
|
|
if (ret != EC_SUCCESS) {
|
|
nvmem_write_error = 1;
|
|
return ret;
|
|
}
|
|
|
|
/* Compute partition offset for destination */
|
|
ret = nvmem_get_partition_off(user, dest_offset, size, &d_buff_offset);
|
|
if (ret != EC_SUCCESS) {
|
|
nvmem_write_error = 1;
|
|
return ret;
|
|
}
|
|
|
|
base_addr = (uintptr_t)nvmem_cache;
|
|
/* Create pointer to src location within partition */
|
|
p_src = (uint8_t *)(base_addr + s_buff_offset);
|
|
/* Create pointer to dest location within partition */
|
|
p_dest = (uint8_t *)(base_addr + d_buff_offset);
|
|
/* Move the data block in NvMem */
|
|
memmove(p_dest, p_src, size);
|
|
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
int nvmem_enable_commits(void)
|
|
{
|
|
if (commits_enabled)
|
|
return EC_SUCCESS;
|
|
|
|
if (nvmem_mutex.task != task_get_current()) {
|
|
CPRINTF("%s: locked by task %d, attempt to unlock by task %d\n",
|
|
__func__, nvmem_mutex.task, task_get_current());
|
|
return EC_ERROR_INVAL;
|
|
}
|
|
|
|
commits_enabled = 1;
|
|
CPRINTS("Committing NVMEM changes.");
|
|
return nvmem_commit();
|
|
}
|
|
|
|
void nvmem_disable_commits(void)
|
|
{
|
|
/* Will be unlocked when nvmem_enable_commits() is called. */
|
|
nvmem_lock_cache();
|
|
|
|
commits_enabled = 0;
|
|
}
|
|
|
|
int nvmem_commit(void)
|
|
{
|
|
if (nvmem_mutex.task == TASK_ID_COUNT) {
|
|
CPRINTF("%s: attempt to commit in unlocked state\n",
|
|
__func__, nvmem_mutex.task);
|
|
return EC_ERROR_OVERFLOW; /* Noting to commit. */
|
|
}
|
|
|
|
if (nvmem_mutex.task != task_get_current()) {
|
|
CPRINTF("%s: locked by task %d, attempt to unlock by task %d\n",
|
|
__func__, nvmem_mutex.task, task_get_current());
|
|
return EC_ERROR_INVAL;
|
|
}
|
|
|
|
/* Ensure that all writes/moves prior to commit call succeeded */
|
|
if (nvmem_write_error) {
|
|
CPRINTS("%s: Write Error, commit abandoned", __func__);
|
|
/* Clear error state */
|
|
nvmem_write_error = 0;
|
|
commits_enabled = 1;
|
|
nvmem_release_cache();
|
|
return EC_ERROR_UNKNOWN;
|
|
}
|
|
|
|
if (!commits_enabled) {
|
|
CPRINTS("Skipping commit");
|
|
return EC_SUCCESS;
|
|
}
|
|
|
|
/* Write active partition to NvMem */
|
|
return nvmem_save();
|
|
}
|