security/vboot: Add vboot callbacks to support EC software sync

Use the new functions introduced into the EC driver to support
performing EC software sync via vboot callbacks.

NOTE: This patch assumes that the EC image is added to CBFS
uncompressed.  Streaming decompression of the image will be added in a
future patch.

Also adds a new Kconfig option VBOOT_EARLY_EC_SYNC.  The new Kconfig
option compiles EC software sync into romstage, dependent upon having a
CrOS EC.

BUG=b:112198832
BRANCH=none
TEST=Successful EC software sync

Change-Id: I9b1458a45ab3ed5623af50f78036c4f88461b226
Signed-off-by: Tim Wawrzynczak <twawrzynczak@chromium.org>
Reviewed-on: https://review.coreboot.org/c/coreboot/+/36208
Reviewed-by: Julius Werner <jwerner@chromium.org>
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
diff --git a/src/security/vboot/ec_sync.c b/src/security/vboot/ec_sync.c
new file mode 100644
index 0000000..ec048aa
--- /dev/null
+++ b/src/security/vboot/ec_sync.c
@@ -0,0 +1,549 @@
+/*
+ * This file is part of the coreboot project.
+ *
+ * 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 <assert.h>
+#include <cbfs.h>
+#include <console/console.h>
+#include <delay.h>
+#include <ec/google/chromeec/ec.h>
+#include <security/vboot/misc.h>
+#include <security/vboot/vbnv.h>
+#include <security/vboot/vboot_common.h>
+#include <timer.h>
+#include <vb2_api.h>
+
+#define _EC_FILENAME(select, suffix) \
+	(select == VB_SELECT_FIRMWARE_READONLY ? "ecro" suffix : "ecrw" suffix)
+#define EC_IMAGE_FILENAME(select) _EC_FILENAME(select, "")
+#define EC_HASH_FILENAME(select) _EC_FILENAME(select, ".hash")
+
+/* Wait 10 ms between attempts to check if EC's hash is ready */
+#define CROS_EC_HASH_CHECK_DELAY_MS 10
+/* Give the EC 2 seconds to finish calculating its hash */
+#define CROS_EC_HASH_TIMEOUT_MS 2000
+
+/* Wait 3 seconds after software sync for EC to clear the limit power flag. */
+#define LIMIT_POWER_WAIT_TIMEOUT_MS 3000
+/* Check the limit power flag every 10 ms while waiting. */
+#define LIMIT_POWER_POLL_SLEEP_MS 10
+
+/* Wait 3 seconds for EC to sysjump to RW */
+#define CROS_EC_SYSJUMP_TIMEOUT_MS 3000
+
+/*
+ * The external API for EC software sync.  This function calls into
+ * vboot, which kicks off the process.  Vboot runs the verified boot
+ * logic, and requires the client program to provide callbacks which
+ * perform the work.
+ */
+void vboot_sync_ec(void)
+{
+	vb2_error_t retval = VB2_SUCCESS;
+	struct vb2_context *ctx;
+
+	ctx = vboot_get_context();
+	ctx->flags |= VB2_CONTEXT_EC_SYNC_SUPPORTED;
+
+	retval = vb2api_ec_sync(ctx);
+	vboot_save_nvdata_only(ctx);
+
+	if (retval != VB2_SUCCESS) {
+		printk(BIOS_ERR, "EC software sync failed (%#x), rebooting\n", retval);
+		vboot_reboot();
+	}
+}
+
+/* Convert firmware image type into a flash offset */
+static uint32_t get_vboot_hash_offset(enum vb2_firmware_selection select)
+{
+	switch (select) {
+	case VB_SELECT_FIRMWARE_READONLY:
+		return EC_VBOOT_HASH_OFFSET_RO;
+	case VB_SELECT_FIRMWARE_EC_UPDATE:
+		return EC_VBOOT_HASH_OFFSET_UPDATE;
+	default:
+		return EC_VBOOT_HASH_OFFSET_ACTIVE;
+	}
+}
+
+/*
+ * Asks the EC to calculate a hash of the specified firmware image, and
+ * returns the information in **hash and *hash_size.
+ */
+static vb2_error_t ec_hash_image(enum vb2_firmware_selection select,
+				 const uint8_t **hash, int *hash_size)
+{
+	static struct ec_response_vboot_hash resp;
+	uint32_t hash_offset;
+	int recalc_requested = 0;
+	struct stopwatch sw;
+
+	hash_offset = get_vboot_hash_offset(select);
+
+	stopwatch_init_msecs_expire(&sw, CROS_EC_HASH_TIMEOUT_MS);
+	do {
+		if (google_chromeec_get_vboot_hash(hash_offset, &resp))
+			return VB2_ERROR_UNKNOWN;
+
+		switch (resp.status) {
+		case EC_VBOOT_HASH_STATUS_NONE:
+			/*
+			 * There is no hash available right now.
+			 * Request a recalc if it hasn't been done yet.
+			 */
+			if (recalc_requested)
+				break;
+
+			printk(BIOS_WARNING,
+			       "%s: No valid hash (status=%d size=%d). "
+			       "Computing...\n", __func__, resp.status,
+			       resp.size);
+
+			if (google_chromeec_start_vboot_hash(
+				    EC_VBOOT_HASH_TYPE_SHA256, hash_offset, &resp))
+				return VB2_ERROR_UNKNOWN;
+
+			recalc_requested = 1;
+
+			/*
+			 * Expect status to be busy since we just sent
+			 * a recalc request.
+			 */
+			resp.status = EC_VBOOT_HASH_STATUS_BUSY;
+
+			/* Hash just started calculating, let it go for a bit */
+			mdelay(CROS_EC_HASH_CHECK_DELAY_MS);
+			break;
+
+		case EC_VBOOT_HASH_STATUS_BUSY:
+			/* Hash is still calculating. */
+			mdelay(CROS_EC_HASH_CHECK_DELAY_MS);
+			break;
+
+		case EC_VBOOT_HASH_STATUS_DONE: /* intentional fallthrough */
+		default:
+			/* Hash is ready! */
+			break;
+		}
+	} while (resp.status == EC_VBOOT_HASH_STATUS_BUSY &&
+		 !stopwatch_expired(&sw));
+
+	if (resp.status != EC_VBOOT_HASH_STATUS_DONE) {
+		printk(BIOS_ERR, "%s: Hash status not done: %d\n", __func__,
+		       resp.status);
+		return VB2_ERROR_UNKNOWN;
+	}
+	if (resp.hash_type != EC_VBOOT_HASH_TYPE_SHA256) {
+		printk(BIOS_ERR, "EC hash was the wrong type.\n");
+		return VB2_ERROR_UNKNOWN;
+	}
+
+	printk(BIOS_INFO, "EC took %luus to calculate image hash\n",
+		stopwatch_duration_usecs(&sw));
+
+	*hash = resp.hash_digest;
+	*hash_size = resp.digest_size;
+
+	return VB2_SUCCESS;
+}
+
+/*
+ * Asks the EC to protect or unprotect the specified flash region.
+ */
+static vb2_error_t ec_protect_flash(enum vb2_firmware_selection select, int enable)
+{
+	struct ec_response_flash_protect resp;
+	uint32_t protected_region = EC_FLASH_PROTECT_ALL_NOW;
+	const uint32_t mask = EC_FLASH_PROTECT_ALL_NOW | EC_FLASH_PROTECT_ALL_AT_BOOT;
+
+	if (select == VB_SELECT_FIRMWARE_READONLY)
+		protected_region = EC_FLASH_PROTECT_RO_NOW;
+
+	if (google_chromeec_flash_protect(mask, enable ? mask : 0, &resp) != 0)
+		return VB2_ERROR_UNKNOWN;
+
+	if (!enable) {
+		/* If protection is still enabled, need reboot */
+		if (resp.flags & protected_region)
+			return VBERROR_EC_REBOOT_TO_RO_REQUIRED;
+
+		return VB2_SUCCESS;
+	}
+
+	/*
+	 * If write protect and ro-at-boot aren't both asserted, don't expect
+	 * protection enabled.
+	 */
+	if ((~resp.flags) & (EC_FLASH_PROTECT_GPIO_ASSERTED |
+			     EC_FLASH_PROTECT_RO_AT_BOOT))
+		return VB2_SUCCESS;
+
+	/* If flash is protected now, success */
+	if (resp.flags & EC_FLASH_PROTECT_ALL_NOW)
+		return VB2_SUCCESS;
+
+	/* If RW will be protected at boot but not now, need a reboot */
+	if (resp.flags & EC_FLASH_PROTECT_ALL_AT_BOOT)
+		return VBERROR_EC_REBOOT_TO_RO_REQUIRED;
+
+	/* Otherwise, it's an error */
+	return VB2_ERROR_UNKNOWN;
+}
+
+/* Convert a firmware image type to an EC flash region */
+static enum ec_flash_region vboot_to_ec_region(enum vb2_firmware_selection select)
+{
+	switch (select) {
+	case VB_SELECT_FIRMWARE_READONLY:
+		return EC_FLASH_REGION_WP_RO;
+	case VB_SELECT_FIRMWARE_EC_UPDATE:
+		return EC_FLASH_REGION_UPDATE;
+	default:
+		return EC_FLASH_REGION_ACTIVE;
+	}
+}
+
+/*
+ * Read the EC's burst size bytes at a time from CBFS, and then send
+ * the chunk to the EC for it to write into its flash.
+ */
+static vb2_error_t ec_flash_write(struct region_device *image_region,
+				  uint32_t region_offset, int image_size)
+{
+	struct ec_response_get_protocol_info resp_proto;
+	struct ec_response_flash_info resp_flash;
+	ssize_t pdata_max_size;
+	ssize_t burst;
+	uint8_t *file_buf;
+	struct ec_params_flash_write *params;
+	uint32_t end, off;
+
+	/*
+	 * Get EC's protocol information, so that we can figure out how much
+	 * data can be sent in one message.
+	 */
+	if (google_chromeec_get_protocol_info(&resp_proto)) {
+		printk(BIOS_ERR, "Failed to get EC protocol information; "
+		       "skipping flash write\n");
+		return VB2_ERROR_UNKNOWN;
+	}
+
+	/*
+	 * Determine burst size.  This must be a multiple of the write block
+	 * size, and must also fit into the host parameter buffer.
+	 */
+	if (google_chromeec_flash_info(&resp_flash)) {
+		printk(BIOS_ERR, "Failed to get EC flash information; "
+		       "skipping flash write\n");
+		return VB2_ERROR_UNKNOWN;
+	}
+
+	/* Limit the potential buffer stack allocation to 1K */
+	pdata_max_size = MIN(1024, resp_proto.max_request_packet_size -
+				   sizeof(struct ec_host_request));
+
+	/* Round burst to a multiple of the flash write block size */
+	burst = pdata_max_size - sizeof(*params);
+	burst = (burst / resp_flash.write_block_size) *
+		resp_flash.write_block_size;
+
+	/* Buffer too small */
+	if (burst <= 0) {
+		printk(BIOS_ERR, "Flash write buffer too small!  skipping "
+		       "flash write\n");
+		return VB2_ERROR_UNKNOWN;
+	}
+
+	/* Allocate buffer on the stack */
+	params = alloca(burst + sizeof(*params));
+
+	/* Fill up the buffer */
+	end = region_offset + image_size;
+	for (off = region_offset; off < end; off += burst) {
+		uint32_t todo = MIN(end - off, burst);
+		uint32_t xfer_size = todo + sizeof(*params);
+
+		/* Map 'todo' bytes into memory */
+		file_buf = rdev_mmap(image_region, off - region_offset, todo);
+		if (file_buf == NULL)
+			return VB2_ERROR_UNKNOWN;
+
+		params->offset = off;
+		params->size = todo;
+
+		/* Read todo bytes into the buffer */
+		memcpy(params + 1, file_buf, todo);
+
+		if (rdev_munmap(image_region, file_buf))
+			return VB2_ERROR_UNKNOWN;
+
+		/* Make sure to add back in the size of the parameters */
+		if (google_chromeec_flash_write_block(
+				(const uint8_t *)params, xfer_size)) {
+			printk(BIOS_ERR, "EC failed flash write command, "
+				"relative offset %u!\n", off - region_offset);
+			return VB2_ERROR_UNKNOWN;
+		}
+	}
+
+	return VB2_SUCCESS;
+}
+
+/*
+ * The logic for updating an EC firmware image.
+ */
+static vb2_error_t ec_update_image(enum vb2_firmware_selection select)
+{
+	uint32_t region_offset, region_size;
+	enum ec_flash_region region;
+	vb2_error_t rv;
+	size_t image_size;
+	struct cbfsf fh;
+	const char *filename;
+	struct region_device image_region;
+
+	/* Un-protect the flash region */
+	rv = ec_protect_flash(select, 0);
+	if (rv != VB2_SUCCESS)
+		return rv;
+
+	/* Convert vboot region into an EC region */
+	region = vboot_to_ec_region(select);
+
+	/* Get information about the flash region */
+	if (google_chromeec_flash_region_info(region, &region_offset,
+					      &region_size))
+		return VB2_ERROR_UNKNOWN;
+
+	/* Locate the CBFS file */
+	filename = EC_IMAGE_FILENAME(select);
+	if (cbfs_boot_locate(&fh, filename, NULL))
+		return VB2_ERROR_UNKNOWN;
+
+	/* Get the file size and the region struct */
+	image_size = region_device_sz(&fh.data);
+	cbfs_file_data(&image_region, &fh);
+
+	/* Bail if the image is too large */
+	if (image_size > region_size)
+		return VB2_ERROR_INVALID_PARAMETER;
+
+	/* Erase the region */
+	if (google_chromeec_flash_erase(region_offset, region_size))
+		return VB2_ERROR_UNKNOWN;
+
+	/* Write the image into the region */
+	if (ec_flash_write(&image_region, region_offset, image_size))
+		return VB2_ERROR_UNKNOWN;
+
+	/* Verify the image */
+	if (google_chromeec_efs_verify(region))
+		return VB2_ERROR_UNKNOWN;
+
+	return VB2_SUCCESS;
+}
+
+static vb2_error_t ec_get_expected_hash(enum vb2_firmware_selection select,
+					const uint8_t **hash,
+					int *hash_size)
+{
+	size_t size;
+	const char *filename = EC_HASH_FILENAME(select);
+	const uint8_t *file = cbfs_boot_map_with_leak(filename, CBFS_TYPE_RAW, &size);
+
+	if (file == NULL)
+		return VB2_ERROR_UNKNOWN;
+
+	*hash = file;
+	*hash_size = (int)size;
+
+	return VB2_SUCCESS;
+}
+
+/***********************************************************************
+ * Vboot Callbacks
+ ***********************************************************************/
+
+/*
+ * Unsupported.
+ *
+ * coreboot does not support the graphics initialization needed to
+ * display the vboot "wait" screens, etc., because the use case for
+ * supporting software sync early in the boot flow is to be able to
+ * quickly update the EC and/or sysjump to RW earlier so that USB-PD
+ * power (> 15 W) can be negotiated for earlier.
+ */
+vb2_error_t VbExDisplayScreen(uint32_t screen_type, uint32_t locale,
+			      const VbScreenData *data)
+{
+	return VB2_ERROR_UNKNOWN;
+}
+
+/*
+ * Write opaque data into NV storage region.
+ */
+vb2_error_t VbExNvStorageWrite(const uint8_t *buf)
+{
+	save_vbnv(buf);
+	return VB2_SUCCESS;
+}
+
+/*
+ * Report whether the EC is in RW or not.
+ */
+vb2_error_t vb2ex_ec_running_rw(int *in_rw)
+{
+	*in_rw = !google_ec_running_ro();
+	return VB2_SUCCESS;
+}
+
+/*
+ * Callback for when Vboot is finished.
+ */
+vb2_error_t vb2ex_ec_vboot_done(struct vb2_context *ctx)
+{
+	int limit_power = 0;
+	bool message_printed = false;
+	struct stopwatch sw;
+	vb2_error_t rv = VB2_SUCCESS;
+	int in_recovery = !!(ctx->flags & VB2_CONTEXT_RECOVERY_MODE);
+
+	/*
+	 * Do not wait for the limit power flag to be cleared in
+	 * recovery mode since we didn't just sysjump.
+	 */
+	if (in_recovery)
+		return VB2_SUCCESS;
+
+	stopwatch_init_msecs_expire(&sw, LIMIT_POWER_WAIT_TIMEOUT_MS);
+
+	/* Ensure we have enough power to continue booting */
+	while (1) {
+		if (google_chromeec_read_limit_power_request(&limit_power)) {
+			printk(BIOS_ERR, "Failed to check EC limit power"
+			       "flag.\n");
+			rv = VB2_ERROR_UNKNOWN;
+			break;
+		}
+
+		if (!limit_power || stopwatch_expired(&sw))
+			break;
+
+		if (!message_printed) {
+			printk(BIOS_SPEW,
+			       "Waiting for EC to clear limit power flag.\n");
+			message_printed = true;
+		}
+
+		mdelay(LIMIT_POWER_POLL_SLEEP_MS);
+	}
+
+	if (limit_power) {
+		printk(BIOS_INFO,
+		       "EC requests limited power usage. Request shutdown.\n");
+		rv = VBERROR_SHUTDOWN_REQUESTED;
+	} else {
+		printk(BIOS_INFO, "Waited %luus to clear limit power flag.\n",
+			stopwatch_duration_usecs(&sw));
+	}
+
+	return rv;
+}
+
+/*
+ * Support battery cutoff if required.
+ */
+vb2_error_t vb2ex_ec_battery_cutoff(void)
+{
+	if (google_chromeec_battery_cutoff(EC_BATTERY_CUTOFF_FLAG_AT_SHUTDOWN))
+		return VB2_ERROR_UNKNOWN;
+
+	return VB2_SUCCESS;
+}
+
+/*
+ * Vboot callback for calculating an EC image's hash.
+ */
+vb2_error_t vb2ex_ec_hash_image(enum vb2_firmware_selection select,
+				const uint8_t **hash, int *hash_size)
+{
+	return ec_hash_image(select, hash, hash_size);
+}
+
+/*
+ * Vboot callback for EC flash protection.
+ */
+vb2_error_t vb2ex_ec_protect(enum vb2_firmware_selection select)
+{
+	return ec_protect_flash(select, 1);
+}
+
+/*
+ * Get hash for image.
+ */
+vb2_error_t vb2ex_ec_get_expected_image_hash(enum vb2_firmware_selection select,
+					     const uint8_t **hash,
+					     int *hash_size)
+{
+	return ec_get_expected_hash(select, hash, hash_size);
+}
+
+/*
+ * Disable further sysjumps (i.e., stay in RW until next reboot)
+ */
+vb2_error_t vb2ex_ec_disable_jump(void)
+{
+	if (google_chromeec_reboot(0, EC_REBOOT_DISABLE_JUMP, 0))
+		return VB2_ERROR_UNKNOWN;
+
+	return VB2_SUCCESS;
+}
+
+/*
+ * Update EC image.
+ */
+vb2_error_t vb2ex_ec_update_image(enum vb2_firmware_selection select)
+{
+	return ec_update_image(select);
+}
+
+/*
+ * Vboot callback for commanding EC to sysjump to RW.
+ */
+vb2_error_t vb2ex_ec_jump_to_rw(void)
+{
+	struct stopwatch sw;
+
+	if (google_chromeec_reboot(0, EC_REBOOT_JUMP_RW, 0))
+		return VB2_ERROR_UNKNOWN;
+
+	/* Give the EC 3 seconds to sysjump */
+	stopwatch_init_msecs_expire(&sw, CROS_EC_SYSJUMP_TIMEOUT_MS);
+
+	/* Default delay to wait after EC reboot */
+	mdelay(50);
+	while (google_chromeec_hello()) {
+		if (stopwatch_expired(&sw)) {
+			printk(BIOS_ERR, "EC did not return from reboot after %luus\n",
+			       stopwatch_duration_usecs(&sw));
+			return VB2_ERROR_UNKNOWN;
+		}
+
+		mdelay(5);
+	}
+
+	printk(BIOS_INFO, "\nEC returned from reboot after %luus\n",
+	       stopwatch_duration_usecs(&sw));
+
+	return VB2_SUCCESS;
+}