diff options
Diffstat (limited to 'src/drivers/mrc_cache/mrc_cache.c')
-rw-r--r-- | src/drivers/mrc_cache/mrc_cache.c | 581 |
1 files changed, 581 insertions, 0 deletions
diff --git a/src/drivers/mrc_cache/mrc_cache.c b/src/drivers/mrc_cache/mrc_cache.c new file mode 100644 index 0000000000..3a9689645f --- /dev/null +++ b/src/drivers/mrc_cache/mrc_cache.c @@ -0,0 +1,581 @@ +/* + * This file is part of the coreboot project. + * + * Copyright (C) 2014 Google Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <compiler.h> +#include <string.h> +#include <boot_device.h> +#include <bootstate.h> +#include <bootmode.h> +#include <console/console.h> +#include <cbmem.h> +#include <elog.h> +#include <fmap.h> +#include <ip_checksum.h> +#include <region_file.h> +#include <security/vboot/vboot_common.h> +#include <spi_flash.h> + +#include "mrc_cache.h" + +#define DEFAULT_MRC_CACHE "RW_MRC_CACHE" +#define VARIABLE_MRC_CACHE "RW_VAR_MRC_CACHE" +#define RECOVERY_MRC_CACHE "RECOVERY_MRC_CACHE" +#define UNIFIED_MRC_CACHE "UNIFIED_MRC_CACHE" + +#define MRC_DATA_SIGNATURE (('M'<<0)|('R'<<8)|('C'<<16)|('D'<<24)) + +struct mrc_metadata { + uint32_t signature; + uint32_t data_size; + uint16_t data_checksum; + uint16_t header_checksum; + uint32_t version; +} __packed; + +enum result { + UPDATE_FAILURE = -1, + UPDATE_SUCCESS = 0, + ALREADY_UPTODATE = 1 +}; + +#define NORMAL_FLAG (1 << 0) +#define RECOVERY_FLAG (1 << 1) + +struct cache_region { + const char *name; + uint32_t cbmem_id; + int type; + int elog_slot; + int flags; +}; + +static const struct cache_region recovery_training = { + .name = RECOVERY_MRC_CACHE, + .cbmem_id = CBMEM_ID_MRCDATA, + .type = MRC_TRAINING_DATA, + .elog_slot = ELOG_MEM_CACHE_UPDATE_SLOT_RECOVERY, +#if IS_ENABLED(CONFIG_HAS_RECOVERY_MRC_CACHE) + .flags = RECOVERY_FLAG, +#else + .flags = 0, +#endif +}; + +static const struct cache_region normal_training = { + .name = DEFAULT_MRC_CACHE, + .cbmem_id = CBMEM_ID_MRCDATA, + .type = MRC_TRAINING_DATA, + .elog_slot = ELOG_MEM_CACHE_UPDATE_SLOT_NORMAL, + .flags = NORMAL_FLAG | RECOVERY_FLAG, +}; + +static const struct cache_region variable_data = { + .name = VARIABLE_MRC_CACHE, + .cbmem_id = CBMEM_ID_VAR_MRCDATA, + .type = MRC_VARIABLE_DATA, + .elog_slot = ELOG_MEM_CACHE_UPDATE_SLOT_VARIABLE, + .flags = NORMAL_FLAG | RECOVERY_FLAG, +}; + +/* Order matters here for priority in matching. */ +static const struct cache_region *cache_regions[] = { + &recovery_training, + &normal_training, + &variable_data, +}; + +static int lookup_region_by_name(const char *name, struct region *r) +{ + /* This assumes memory mapped boot media just under 4GiB. */ + const uint32_t pointer_base_32bit = -CONFIG_ROM_SIZE; + + if (fmap_locate_area(name, r) == 0) + return 0; + + /* CHROMEOS builds must get their MRC cache from FMAP. */ + if (IS_ENABLED(CONFIG_CHROMEOS)) { + printk(BIOS_ERR, "MRC: Chrome OS lookup failure.\n"); + return -1; + } + + if (!IS_ENABLED(CONFIG_BOOT_DEVICE_MEMORY_MAPPED)) + return -1; + + /* Base is in the form of a pointer. Make it an offset. */ + r->offset = CONFIG_MRC_SETTINGS_CACHE_BASE - pointer_base_32bit; + r->size = CONFIG_MRC_SETTINGS_CACHE_SIZE; + + return 0; +} + +static const struct cache_region *lookup_region_type(int type) +{ + int i; + int flags; + + if (vboot_recovery_mode_enabled()) + flags = RECOVERY_FLAG; + else + flags = NORMAL_FLAG; + + for (i = 0; i < ARRAY_SIZE(cache_regions); i++) { + if (cache_regions[i]->type != type) + continue; + if ((cache_regions[i]->flags & flags) == flags) + return cache_regions[i]; + } + + return NULL; +} + +int mrc_cache_stash_data(int type, uint32_t version, const void *data, + size_t size) +{ + const struct cache_region *cr; + size_t cbmem_size; + struct mrc_metadata *md; + + cr = lookup_region_type(type); + if (cr == NULL) { + printk(BIOS_ERR, "MRC: failed to add to cbmem for type %d.\n", + type); + return -1; + } + + cbmem_size = sizeof(*md) + size; + + md = cbmem_add(cr->cbmem_id, cbmem_size); + + if (md == NULL) { + printk(BIOS_ERR, "MRC: failed to add '%s' to cbmem.\n", + cr->name); + return -1; + } + + memset(md, 0, sizeof(*md)); + md->signature = MRC_DATA_SIGNATURE; + md->data_size = size; + md->version = version; + md->data_checksum = compute_ip_checksum(data, size); + md->header_checksum = compute_ip_checksum(md, sizeof(*md)); + memcpy(&md[1], data, size); + + return 0; +} + +static const struct cache_region *lookup_region(struct region *r, int type) +{ + const struct cache_region *cr; + + cr = lookup_region_type(type); + + if (cr == NULL) { + printk(BIOS_ERR, "MRC: failed to locate region type %d.\n", + type); + return NULL; + } + + if (lookup_region_by_name(cr->name, r) < 0) + return NULL; + + return cr; +} + +static int mrc_header_valid(struct region_device *rdev, struct mrc_metadata *md) +{ + uint16_t checksum; + uint16_t checksum_result; + size_t size; + + if (rdev_readat(rdev, md, 0, sizeof(*md)) < 0) { + printk(BIOS_ERR, "MRC: couldn't read metadata\n"); + return -1; + } + + if (md->signature != MRC_DATA_SIGNATURE) { + printk(BIOS_ERR, "MRC: invalid header signature\n"); + return -1; + } + + /* Compute checksum over header with 0 as the value. */ + checksum = md->header_checksum; + md->header_checksum = 0; + checksum_result = compute_ip_checksum(md, sizeof(*md)); + + if (checksum != checksum_result) { + printk(BIOS_ERR, "MRC: header checksum mismatch: %x vs %x\n", + checksum, checksum_result); + return -1; + } + + /* Put back original. */ + md->header_checksum = checksum; + + /* Re-size the region device according to the metadata as a region_file + * does block allocation. */ + size = sizeof(*md) + md->data_size; + if (rdev_chain(rdev, rdev, 0, size) < 0) { + printk(BIOS_ERR, "MRC: size exceeds rdev size: %zx vs %zx\n", + size, region_device_sz(rdev)); + return -1; + } + + return 0; +} + +static int mrc_data_valid(const struct region_device *rdev, + const struct mrc_metadata *md) +{ + void *data; + uint16_t checksum; + const size_t md_size = sizeof(*md); + const size_t data_size = md->data_size; + + data = rdev_mmap(rdev, md_size, data_size); + if (data == NULL) { + printk(BIOS_ERR, "MRC: mmap failure on data verification.\n"); + return -1; + } + + checksum = compute_ip_checksum(data, data_size); + + rdev_munmap(rdev, data); + if (md->data_checksum != checksum) { + printk(BIOS_ERR, "MRC: data checksum mismatch: %x vs %x\n", + md->data_checksum, checksum); + return -1; + } + + return 0; +} + +static int mrc_cache_latest(const char *name, + const struct region_device *backing_rdev, + struct mrc_metadata *md, + struct region_file *cache_file, + struct region_device *rdev, + bool fail_bad_data) +{ + /* Init and obtain a handle to the file data. */ + if (region_file_init(cache_file, backing_rdev) < 0) { + printk(BIOS_ERR, "MRC: region file invalid in '%s'\n", name); + return -1; + } + + /* Provide a 0 sized region_device from here on out so the caller + * has a valid yet unusable region_device. */ + rdev_chain(rdev, backing_rdev, 0, 0); + + /* No data to return. */ + if (region_file_data(cache_file, rdev) < 0) { + printk(BIOS_ERR, "MRC: no data in '%s'\n", name); + return fail_bad_data ? -1 : 0; + } + + /* Validate header and resize region to reflect actual usage on the + * saved medium (including metadata and data). */ + if (mrc_header_valid(rdev, md) < 0) { + printk(BIOS_ERR, "MRC: invalid header in '%s'\n", name); + return fail_bad_data ? -1 : 0; + } + + /* Validate Data */ + if (mrc_data_valid(rdev, md) < 0) { + printk(BIOS_ERR, "MRC: invalid data in '%s'\n", name); + return fail_bad_data ? -1 : 0; + } + + return 0; +} + +int mrc_cache_get_current(int type, uint32_t version, + struct region_device *rdev) +{ + const struct cache_region *cr; + struct region region; + struct region_device read_rdev; + struct region_file cache_file; + struct mrc_metadata md; + size_t data_size; + const size_t md_size = sizeof(md); + const bool fail_bad_data = true; + + cr = lookup_region(®ion, type); + + if (cr == NULL) + return -1; + + if (boot_device_ro_subregion(®ion, &read_rdev) < 0) + return -1; + + if (mrc_cache_latest(cr->name, &read_rdev, &md, &cache_file, rdev, + fail_bad_data) < 0) + return -1; + + if (version != md.version) { + printk(BIOS_INFO, "MRC: version mismatch: %x vs %x\n", + md.version, version); + return -1; + } + + /* Re-size rdev to only contain the data. i.e. remove metadata. */ + data_size = md.data_size; + return rdev_chain(rdev, rdev, md_size, data_size); +} + +static bool mrc_cache_needs_update(const struct region_device *rdev, + const struct cbmem_entry *to_be_updated) +{ + void *mapping; + size_t size = region_device_sz(rdev); + bool need_update = false; + + if (cbmem_entry_size(to_be_updated) != size) + return true; + + mapping = rdev_mmap_full(rdev); + + if (memcmp(cbmem_entry_start(to_be_updated), mapping, size)) + need_update = true; + + rdev_munmap(rdev, mapping); + + return need_update; +} + +static void log_event_cache_update(uint8_t slot, enum result res) +{ + const int type = ELOG_TYPE_MEM_CACHE_UPDATE; + struct elog_event_mem_cache_update event = { + .slot = slot + }; + + /* Filter through interesting events only */ + switch (res) { + case UPDATE_FAILURE: + event.status = ELOG_MEM_CACHE_UPDATE_STATUS_FAIL; + break; + case UPDATE_SUCCESS: + event.status = ELOG_MEM_CACHE_UPDATE_STATUS_SUCCESS; + break; + default: + return; + } + + if (elog_add_event_raw(type, &event, sizeof(event)) < 0) + printk(BIOS_ERR, "Failed to log mem cache update event.\n"); +} + +/* During ramstage this code purposefully uses incoherent transactions between + * read and write. The read assumes a memory-mapped boot device that can be used + * to quickly locate and compare the up-to-date data. However, when an update + * is required it uses the writeable region access to perform the update. */ +static void update_mrc_cache_by_type(int type) +{ + const struct cache_region *cr; + struct region region; + struct region_device read_rdev; + struct region_device write_rdev; + struct region_file cache_file; + struct mrc_metadata md; + const struct cbmem_entry *to_be_updated; + struct incoherent_rdev backing_irdev; + const struct region_device *backing_rdev; + struct region_device latest_rdev; + const bool fail_bad_data = false; + + cr = lookup_region(®ion, type); + + if (cr == NULL) + return; + + to_be_updated = cbmem_entry_find(cr->cbmem_id); + if (to_be_updated == NULL) { + printk(BIOS_ERR, "MRC: No data in cbmem for '%s'.\n", + cr->name); + return; + } + + printk(BIOS_DEBUG, "MRC: Checking cached data update for '%s'.\n", + cr->name); + + if (boot_device_ro_subregion(®ion, &read_rdev) < 0) + return; + + if (boot_device_rw_subregion(®ion, &write_rdev) < 0) + return; + + backing_rdev = incoherent_rdev_init(&backing_irdev, ®ion, &read_rdev, + &write_rdev); + + if (backing_rdev == NULL) + return; + + if (mrc_cache_latest(cr->name, backing_rdev, &md, &cache_file, + &latest_rdev, fail_bad_data) < 0) + return; + + if (!mrc_cache_needs_update(&latest_rdev, to_be_updated)) { + log_event_cache_update(cr->elog_slot, ALREADY_UPTODATE); + return; + } + + printk(BIOS_DEBUG, "MRC: cache data '%s' needs update.\n", cr->name); + + if (region_file_update_data(&cache_file, + cbmem_entry_start(to_be_updated), + cbmem_entry_size(to_be_updated)) < 0) + log_event_cache_update(cr->elog_slot, UPDATE_FAILURE); + else + log_event_cache_update(cr->elog_slot, UPDATE_SUCCESS); +} + +/* Read flash status register to determine if write protect is active */ +static int nvm_is_write_protected(void) +{ + u8 sr1; + u8 wp_gpio; + u8 wp_spi; + + if (!IS_ENABLED(CONFIG_CHROMEOS)) + return 0; + + if (!IS_ENABLED(CONFIG_BOOT_DEVICE_SPI_FLASH)) + return 0; + + /* Read Write Protect GPIO if available */ + wp_gpio = get_write_protect_state(); + + /* Read Status Register 1 */ + if (spi_flash_status(boot_device_spi_flash(), &sr1) < 0) { + printk(BIOS_ERR, "Failed to read SPI status register 1\n"); + return -1; + } + wp_spi = !!(sr1 & 0x80); + + printk(BIOS_DEBUG, "SPI flash protection: WPSW=%d SRP0=%d\n", + wp_gpio, wp_spi); + + return wp_gpio && wp_spi; +} + +/* Apply protection to a range of flash */ +static int nvm_protect(const struct region *r) +{ + if (!IS_ENABLED(CONFIG_MRC_SETTINGS_PROTECT)) + return 0; + + if (!IS_ENABLED(CONFIG_BOOT_DEVICE_SPI_FLASH)) + return 0; + + return spi_flash_ctrlr_protect_region(boot_device_spi_flash(), r); +} + +/* Protect mrc region with a Protected Range Register */ +static int protect_mrc_cache(const char *name) +{ + struct region region; + + if (!IS_ENABLED(CONFIG_MRC_SETTINGS_PROTECT)) + return 0; + + if (lookup_region_by_name(name, ®ion) < 0) { + printk(BIOS_ERR, "MRC: Could not find region '%s'\n", name); + return -1; + } + + if (nvm_is_write_protected() <= 0) { + printk(BIOS_INFO, "MRC: NOT enabling PRR for '%s'.\n", name); + return 0; + } + + if (nvm_protect(®ion) < 0) { + printk(BIOS_ERR, "MRC: ERROR setting PRR for '%s'.\n", name); + return -1; + } + + printk(BIOS_INFO, "MRC: Enabled Protected Range on '%s'.\n", name); + return 0; +} + +static void protect_mrc_region(void) +{ + /* + * Check if there is a single unified region that encompasses both + * RECOVERY_MRC_CACHE and DEFAULT_MRC_CACHE. In that case protect the + * entire region using a single PRR. + * + * If we are not able to protect the entire region, try protecting + * individual regions next. + */ + if (protect_mrc_cache(UNIFIED_MRC_CACHE) == 0) + return; + + if (IS_ENABLED(CONFIG_HAS_RECOVERY_MRC_CACHE)) + protect_mrc_cache(RECOVERY_MRC_CACHE); + + protect_mrc_cache(DEFAULT_MRC_CACHE); +} + +static void invalidate_normal_cache(void) +{ + struct region_file cache_file; + struct region_device rdev; + const char *name = DEFAULT_MRC_CACHE; + const uint32_t invalid = ~MRC_DATA_SIGNATURE; + + /* Invalidate only on recovery mode with retraining enabled. */ + if (!vboot_recovery_mode_enabled()) + return; + if (!vboot_recovery_mode_memory_retrain()) + return; + + if (fmap_locate_area_as_rdev_rw(name, &rdev) < 0) { + printk(BIOS_ERR, "MRC: Couldn't find '%s' region. Invalidation failed\n", + name); + return; + } + + if (region_file_init(&cache_file, &rdev) < 0) { + printk(BIOS_ERR, "MRC: region file invalid for '%s'. Invalidation failed\n", + name); + return; + } + + /* Push an update that consists of 4 bytes that is smaller than the + * MRC metadata as well as an invalid signature. */ + if (region_file_update_data(&cache_file, &invalid, sizeof(invalid)) < 0) + printk(BIOS_ERR, "MRC: invalidation failed for '%s'.\n", name); +} + +static void update_mrc_cache(void *unused) +{ + update_mrc_cache_by_type(MRC_TRAINING_DATA); + + if (IS_ENABLED(CONFIG_MRC_SETTINGS_VARIABLE_DATA)) + update_mrc_cache_by_type(MRC_VARIABLE_DATA); + + if (IS_ENABLED(CONFIG_MRC_CLEAR_NORMAL_CACHE_ON_RECOVERY_RETRAIN)) + invalidate_normal_cache(); + + protect_mrc_region(); +} + +/* + * Ensures MRC training data is stored into SPI after PCI enumeration is done + * during BS_DEV_ENUMERATE-BS_ON_EXIT and lock down SPI protected ranges + * during BS_DEV_RESOURCES-BS_ON_EXIT. + */ +BOOT_STATE_INIT_ENTRY(BS_DEV_ENUMERATE, BS_ON_EXIT, update_mrc_cache, NULL); |