| /* Copyright (c) 2013 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. |
| */ |
| |
| /* *** THIS CODE HAS NOT BEEN SECURITY REVIEWED *** |
| * It lives in the firmware directory because that's where it needs to go |
| * eventually, but at the moment it is used only by usermode tools. |
| * Security review must be completed before this code is used in the |
| * firmware. |
| * See issue 246680 |
| */ |
| |
| #include "flash_ts.h" |
| |
| #include "errno.h" |
| #include "stdio.h" |
| #include "string.h" |
| #include "utility.h" |
| |
| // These match the linux driver |
| #define FLASH_TS_MAGIC 0x53542a46 |
| |
| #define FLASH_TS_HEADER_SIZE 24 |
| #define FLASH_TS_MAX_SIZE 16384 |
| #define FLASH_TS_MAX_ELEMENT_SIZE (FLASH_TS_MAX_SIZE - FLASH_TS_HEADER_SIZE) |
| |
| typedef struct { |
| uint32_t magic; |
| uint32_t crc; |
| uint32_t length; |
| uint32_t version; |
| char data[FLASH_TS_MAX_ELEMENT_SIZE]; |
| } __attribute__((packed)) flash_ts; |
| |
| typedef struct { |
| size_t start_block; // Partition start offset (in erase blocks) |
| size_t end_block; // Partition end offset (in erase blocks) |
| size_t chunk_size; // Minimum element size |
| size_t pages_per_block, chunks_per_block, pages_per_chunk; |
| nand_geom nand; |
| |
| size_t cached_block; |
| size_t current_block; |
| |
| flash_ts current; |
| flash_ts temp; |
| } flash_ts_state; |
| |
| |
| static flash_ts_state state; |
| |
| size_t pow2(size_t x) { |
| size_t v = 1; |
| while (v < x) |
| v <<= 1; |
| return v; |
| } |
| |
| static inline uint32_t flash_ts_crc(const flash_ts *cache) |
| { |
| const unsigned char *p; |
| uint32_t crc = 0; |
| size_t len; |
| |
| /* skip magic and crc fields */ |
| len = cache->length + 2 * sizeof(uint32_t); |
| p = (const unsigned char*)&cache->length; |
| |
| while (len--) { |
| int i; |
| |
| crc ^= *p++; |
| for (i = 0; i < 8; i++) |
| crc = (crc >> 1) ^ ((crc & 1) ? 0xedb88320 : 0); |
| } |
| return crc ^ ~0; |
| } |
| |
| static inline int flash_ts_check_crc(const flash_ts *ts) { |
| return ts->crc == flash_ts_crc(ts); |
| } |
| |
| static int is_blank(const void *ptr, size_t sz) { |
| const unsigned char *p = (const unsigned char*)ptr; |
| const unsigned char *end = p + sz; |
| while (p < end) |
| if (*p++ != 0xff) |
| return 0; |
| return 1; |
| } |
| |
| static int is_pow2(size_t v) { |
| return v && (v & (v - 1)) == 0; |
| } |
| |
| /* Scan the entire partition to find the latest version */ |
| static void flash_ts_scan_partition(flash_ts_state *ts) { |
| size_t block; |
| |
| for (block = ts->start_block; block < ts->end_block; block++) { |
| if (!nand_is_bad_block(&ts->nand, block)) { |
| size_t chunk; |
| size_t page_base = block * ts->pages_per_block; |
| |
| for (chunk = 0; chunk < ts->chunks_per_block; |
| chunk++, page_base += ts->pages_per_chunk) { |
| if (nand_read_page(&ts->nand, page_base, |
| &ts->temp, sizeof(ts->temp))) { |
| continue; |
| } |
| if (ts->temp.magic != FLASH_TS_MAGIC || |
| ts->temp.version <= ts->current.version || |
| !flash_ts_check_crc(&ts->temp)) { |
| if (is_blank(&ts->temp, sizeof(ts->temp))) { |
| // Since we only write sequentially, a blank chunk means no more |
| // data in this block. |
| break; |
| } |
| continue; |
| } |
| |
| // It's good & newer than our current version |
| ts->current_block = block; |
| Memcpy(&ts->current, &ts->temp, sizeof(ts->current)); |
| } |
| } |
| } |
| } |
| |
| static char *flash_ts_search(flash_ts *ts, const char *key) { |
| char *str = &ts->data[0]; |
| size_t keylen = strlen(key); |
| |
| while(*str && str + keylen < &ts->data[ts->length]) { |
| // Format: name=value\0name2=value2\0 ... keyn=valuen\0\0 |
| if (!Memcmp(str, key, keylen) && str[keylen] == '=') { |
| return &str[keylen + 1]; |
| } else { |
| str += strlen(str) + 1; // Skip to next key |
| } |
| } |
| return NULL; |
| } |
| |
| static int flash_ts_find_writeable_chunk(flash_ts_state *ts, uint32_t block) { |
| uint32_t page_base = block * ts->pages_per_block; |
| uint32_t page_end = (block + 1) * ts->pages_per_block; |
| |
| for(; page_base < page_end; page_base += ts->pages_per_chunk) { |
| if(!nand_read_page(&ts->nand, page_base, |
| &ts->temp, sizeof(ts->temp))) { |
| if (is_blank(&ts->temp, sizeof(ts->temp))) |
| return page_base; |
| } |
| } |
| |
| return -1; |
| } |
| |
| static int in_range(const flash_ts_state *ts, uint32_t block) { |
| return block >= ts->start_block && block < ts->end_block; |
| } |
| |
| static int flash_try_write(flash_ts_state *ts, uint32_t page) { |
| return nand_write_page(&ts->nand, page, &ts->current, sizeof(ts->current)) || |
| nand_read_page(&ts->nand, page, &ts->temp, sizeof(ts->temp)) || |
| Memcmp(&ts->current, &ts->temp, sizeof(ts->current)); |
| } |
| |
| |
| static int flash_ts_find_writeable_spot(flash_ts_state *ts, |
| uint32_t *page_ofs) { |
| uint32_t block; |
| if (in_range(ts, ts->cached_block)) { |
| // We have a starting position to scan from |
| block = ts->cached_block; |
| } else { |
| block = ts->start_block; |
| VBDEBUG(("Cached block not in range - starting from %u\n", block)); |
| } |
| for (; block < ts->end_block; block++) { |
| int chunk; |
| if (nand_is_bad_block(&ts->nand, block)) { |
| VBDEBUG(("Skipping bad block %u\n", block)); |
| continue; |
| } |
| |
| chunk = flash_ts_find_writeable_chunk(ts, block); |
| if (chunk < 0) { |
| VBDEBUG(("No free chunks in block %u\n", block)); |
| continue; |
| } |
| |
| VBDEBUG(("Free chunk %d in block %u\n", chunk, block)); |
| *page_ofs = chunk; |
| ts->cached_block = block; |
| return 0; |
| } |
| return -1; |
| } |
| |
| static int flash_try_erase(flash_ts_state *ts, int block) { |
| return nand_is_bad_block(&ts->nand, block) || |
| nand_erase_block(&ts->nand, block); |
| } |
| |
| static int flash_erase_any_block(flash_ts_state *ts, uint32_t hint) { |
| uint32_t block; |
| for (block = hint; block < ts->end_block; block++) { |
| if (!flash_try_erase(ts, block)) { |
| ts->cached_block = block; |
| VBDEBUG(("Erased block %u\n", block)); |
| return 0; |
| } |
| } |
| |
| if (hint > ts->end_block) |
| hint = ts->end_block; |
| |
| for (block = ts->start_block; block < hint; block++) { |
| if (!flash_try_erase(ts, block)) { |
| ts->cached_block = block; |
| VBDEBUG(("Erased block %u\n", block)); |
| return 0; |
| } |
| } |
| return -1; |
| } |
| |
| static int flash_ts_write(flash_ts_state *ts) { |
| int passes = 3; |
| uint32_t page; |
| |
| |
| ts->cached_block = ts->current_block; |
| ts->current.version++; |
| ts->current.crc = flash_ts_crc(&ts->current); |
| VBDEBUG(("flash_ts_write() - %u bytes, crc %08X\n", |
| ts->current.length, ts->current.crc)); |
| |
| while(passes--) { |
| if (flash_ts_find_writeable_spot(ts, &page)) { |
| if (ts->cached_block == ts->end_block) { |
| uint32_t block; |
| |
| // Partition full! |
| // Erase a block to get some space |
| if (in_range(ts, ts->current_block) && |
| ts->current_block != ts->end_block - 1) { |
| // We don't want to overwrite our good copy if we can avoid it. |
| block = ts->current_block + 1; |
| } else { |
| block = ts->start_block; |
| } |
| VBDEBUG(("Partition full - begin erasing from block %u\n", block)); |
| |
| // Erase block, and try again. |
| if (flash_erase_any_block(ts, block)) { |
| // Failed to erase anything, so abort. |
| VBDEBUG(("All erases failed, aborting\n")); |
| return -ENOMEM; |
| } |
| continue; |
| } else { |
| // Try again, re-scan everything. |
| ts->cached_block = ts->end_block; |
| continue; |
| } |
| } |
| |
| if (flash_try_write(ts, page)) { |
| // Write failure, or read-back failure, try again with the next block. |
| VBDEBUG(("Write failure, retry\n")); |
| ts->cached_block++; |
| continue; |
| } |
| |
| VBDEBUG(("Successfully written v%u @ %u\n", ts->current.version, page)); |
| ts->current_block = ts->cached_block; |
| return 0; |
| } |
| |
| VBDEBUG(("Out of tries\n")); |
| return -EAGAIN; |
| } |
| |
| // Set value, returns 0 on success |
| int flash_ts_set(const char *key, const char *value) { |
| flash_ts *ts = &state.current; |
| char *at; |
| size_t keylen = strlen(key); |
| size_t value_len = strlen(value); |
| |
| if (keylen == 0) { |
| VBDEBUG(("0-length key - illegal\n")); |
| return -1; |
| } |
| |
| if (strchr(key, '=')) { |
| VBDEBUG(("key contains '=' - illegal\n")); |
| return -1; |
| } |
| |
| Memcpy(&state.temp, &state.current, sizeof(state.temp)); |
| |
| at = flash_ts_search(ts, key); |
| if (at) { |
| size_t old_value_len; |
| |
| // Already exists |
| if (!strcmp(at, value)) { |
| // No change |
| VBDEBUG(("Values are the same, not writing\n")); |
| return 0; |
| } |
| |
| old_value_len = strlen(at); |
| if (value_len == old_value_len) { |
| // Overwrite it |
| Memcpy(at, value, value_len); |
| VBDEBUG(("Values are the same length, overwrite\n")); |
| } else { |
| // Remove it |
| // if value_len == 0, then we're done |
| // if value_len != old_value_len, then we do the append below |
| char *src = at - (keylen + 1); |
| char *end = &ts->data[ts->length]; |
| char *from = at + old_value_len + 1; |
| |
| VBDEBUG(("Delete old value\n")); |
| memmove(src, from, end - from); |
| ts->length -= (from-src); |
| ts->data[ts->length - 1] = '\0'; |
| at = NULL; // Enter the append branch below |
| } |
| } else if (value_len == 0) { |
| // Removing non-existent entry |
| return 0; |
| } |
| |
| if (!at && value_len > 0) { |
| // Append it |
| |
| if (ts->length + keylen + 1 + value_len + 1 > FLASH_TS_MAX_ELEMENT_SIZE) { |
| // Not enough space, restore previous |
| VBDEBUG(("Not enough space to write %d data bytes\n", value_len)); |
| Memcpy(&state.current, &state.temp, sizeof(state.temp)); |
| return -1; |
| } |
| |
| VBDEBUG(("Append new value\n")); |
| at = &ts->data[ts->length - 1]; |
| strcpy(at, key); |
| at[keylen] = '='; |
| strcpy(at + keylen + 1, value); |
| ts->length += keylen + 1 + value_len + 1; |
| ts->data[ts->length-1] = '\0'; |
| } |
| |
| return flash_ts_write(&state); |
| } |
| |
| void flash_ts_get(const char *key, char *value, unsigned int size) { |
| flash_ts_state *ts = &state; |
| const char *at; |
| |
| at = flash_ts_search(&ts->current, key); |
| if (at) { |
| strncpy(value, at, size); |
| } else { |
| *value = '\0'; |
| } |
| } |
| |
| int flash_ts_init(unsigned int start_block, unsigned int blocks, |
| unsigned int szofpg, unsigned int szofblk, |
| unsigned int szofsector, void *user) { |
| flash_ts_state *ts = &state; |
| |
| if (!is_pow2(szofpg) || !is_pow2(szofblk) || !is_pow2(szofsector) || |
| szofsector > szofpg || szofpg > szofblk || blocks == 0) |
| return -ENODEV; |
| |
| Memset(ts, 0, sizeof(*ts)); |
| |
| // Page <= chunk <= block |
| // Page is minimum writable unit |
| // Chunk is actual write unit |
| // Block is erase unit |
| ts->start_block = start_block; |
| ts->end_block = start_block + blocks; |
| ts->pages_per_block = szofblk / szofpg; |
| |
| ts->nand.user = user; |
| ts->nand.szofpg = szofpg; |
| ts->nand.szofblk = szofblk; |
| ts->nand.szofsector = szofsector; |
| |
| // Calculate our write size, this mirrors the linux driver's logic |
| ts->chunk_size = pow2((sizeof(flash_ts) + szofpg - 1) & ~(szofpg - 1)); |
| if (!is_pow2(ts->chunk_size)) |
| return -ENODEV; |
| |
| ts->pages_per_chunk = ts->chunk_size / szofpg; |
| if (ts->pages_per_chunk == 0 || ts->chunk_size > szofblk) |
| return -ENODEV; |
| |
| ts->chunks_per_block = szofblk / ts->chunk_size; |
| |
| ts->current.version = 0; |
| ts->current.length = 1; |
| ts->current.magic = FLASH_TS_MAGIC; |
| ts->current.crc = flash_ts_crc(&ts->current); |
| ts->current.data[0] = '\0'; |
| ts->current_block = ts->end_block; |
| |
| flash_ts_scan_partition(ts); |
| |
| return 0; |
| } |