/* 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_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_erase_user_data(enum nvmem_users user) { int part; int ret; uint32_t user_offset, user_size; if (user >= NVMEM_NUM_USERS) return EC_ERROR_INVAL; CPRINTS("Erasing NVMEM Flash Partition user: %d", user); ret = EC_SUCCESS; /* Find offset within cache. */ user_offset = nvmem_user_start_offset[user]; user_size = nvmem_user_sizes[user]; for (part = 0; part < NVMEM_NUM_PARTITIONS; part++) { int rv; /* Lock the cache buffer. */ nvmem_lock_cache(); /* Erase the user's data. */ memset(nvmem_cache + user_offset, 0xFF, user_size); /* * Make sure the contents change between runs of * nvmem_save() so that all flash partitions are * written with empty contents and different * generation numbers. */ ((struct nvmem_partition *)nvmem_cache)->tag.generation = part; /* Make a best effort to clear each partition. */ rv = nvmem_save(); 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(); }