futility: Add `flash` subcommand

Add a new subcommand for getting/setting flash properties such as
the flash size and writeprotect configuration.

The operations provided by `futility flash` require less information
from the user and are less error prone than the equivalents provided by
`flashrom`.

For example, --wp-enable automatically choses the protection range based
on the firmware image and --wp-status gives a warning if the protection
range does not match the RO firmware region.

BUG=b:268574030
BRANCH=none
TEST=`futility flash --{flash-size,wp-enable,wp-disable,wp-status}`

Co-authored-by: Edward O'Callaghan <quasisec@google.com>
Signed-off-by: Edward O'Callaghan <quasisec@google.com>
Signed-off-by: Nikolai Artemiev <nartemiev@google.com>
Change-Id: I36d7468616a5bcdf3c4542d48652bd24c3377a61
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/4279661
Reviewed-by: Edward O'Callaghan <quasisec@chromium.org>
Commit-Queue: Edward O'Callaghan <quasisec@chromium.org>
diff --git a/Android.mk b/Android.mk
index 8708c26..08176cb 100644
--- a/Android.mk
+++ b/Android.mk
@@ -120,6 +120,7 @@
 FUTIL_STATIC_SRCS = \
 	futility/futility.c \
 	futility/cmd_dump_fmap.c \
+	futility/cmd_flash_util.c \
 	futility/cmd_gbb_utility.c \
 	futility/misc.c
 
diff --git a/Makefile b/Makefile
index 8cc68e3..2219efd 100644
--- a/Makefile
+++ b/Makefile
@@ -651,6 +651,7 @@
 	futility/cmd_create.c \
 	futility/cmd_dump_fmap.c \
 	futility/cmd_dump_kernel_config.c \
+	futility/cmd_flash_util.c \
 	futility/cmd_gbb_utility.c \
 	futility/cmd_gscvd.c \
 	futility/cmd_load_fmap.c \
diff --git a/futility/cmd_flash_util.c b/futility/cmd_flash_util.c
new file mode 100644
index 0000000..783a595
--- /dev/null
+++ b/futility/cmd_flash_util.c
@@ -0,0 +1,260 @@
+/* Copyright 2023 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+
+#include "fmap.h"
+#include "futility.h"
+#include "updater.h"
+
+#ifdef USE_FLASHROM
+
+static int print_flash_size(struct updater_config *cfg)
+{
+	uint32_t flash_size;
+	if (flashrom_get_size(cfg->image.programmer, &flash_size,
+			      cfg->verbosity + 1)) {
+		ERROR("%s failed.\n", __func__);
+		return -1;
+	}
+
+	printf("Flash size: %#010x\n", flash_size);
+	return 0;
+}
+
+static int get_ro_range(struct updater_config *cfg,
+			uint32_t *start, uint32_t *len)
+{
+	int ret = 0;
+
+	/* Read fmap */
+	const char *const regions[] = {FMAP_RO_FMAP, NULL};
+	if (flashrom_read_image(&cfg->image_current, regions,
+				cfg->verbosity + 1))
+		return -1;
+
+	FmapAreaHeader *wp_ro = NULL;
+	uint8_t *r = fmap_find_by_name(cfg->image_current.data,
+				       cfg->image_current.size,
+				       NULL, FMAP_RO, &wp_ro);
+	if (!r || !wp_ro) {
+		ERROR("Could not find WP_RO in the FMAP\n");
+		ret = -1;
+		goto err;
+	}
+
+	*start = wp_ro->area_offset;
+	*len = wp_ro->area_size;
+
+err:
+	free(cfg->image_current.data);
+	cfg->image_current.data = NULL;
+	cfg->image_current.size = 0;
+
+	return ret;
+}
+
+static int print_wp_status(struct updater_config *cfg)
+{
+	/* Get WP_RO region start and length from image */
+	uint32_t ro_start, ro_len;
+	if (get_ro_range(cfg, &ro_start, &ro_len))
+		return -1;
+
+	/* Get current WP region and mode from SPI flash */
+	bool wp_mode;
+	uint32_t wp_start, wp_len;
+	if (flashrom_get_wp(cfg->image.programmer, &wp_mode,
+			    &wp_start, &wp_len, cfg->verbosity + 1)) {
+		ERROR("Failed to get WP status\n");
+		return -1;
+	}
+
+	if (!wp_mode && wp_start == 0 && wp_len == 0) {
+		printf("WP status: disabled\n");
+	} else if (wp_mode && wp_start == ro_start && wp_len == ro_len) {
+		printf("WP status: enabled\n");
+	} else {
+		printf("WP status: misconfigured (srp = %d, start = %#010x, length = %#010x)\n",
+		     wp_mode, wp_start, wp_len);
+	}
+
+	return 0;
+}
+
+
+static int set_flash_wp(struct updater_config *cfg, bool enable)
+{
+	uint32_t wp_start = 0;
+	uint32_t wp_len = 0;
+
+	if (enable) {
+		/* Use the WP_RO region as the protection range */
+		if (get_ro_range(cfg, &wp_start, &wp_len))
+			return -1;
+	}
+
+	if (flashrom_set_wp(cfg->image.programmer, enable,
+			    wp_start, wp_len, cfg->verbosity + 1)) {
+		ERROR("Failed to modify WP configuration.\n");
+		return -1;
+	}
+
+	printf("%s WP\n", enable ? "Enabled" : "Disabled");
+
+	return 0;
+}
+
+/* Command line options */
+static struct option const long_opts[] = {
+	SHARED_FLASH_ARGS_LONGOPTS
+	/* name  has_arg *flag val */
+	{"help", 0, NULL, 'h'},
+	{"wp-status", 0, NULL, 's'},
+	{"wp-enable", 0, NULL, 'e'},
+	{"wp-disable", 0, NULL, 'd'},
+	{"flash-size", 0, NULL, 'z'},
+	{NULL, 0, NULL, 0},
+};
+
+static const char *const short_opts = "h" SHARED_FLASH_ARGS_SHORTOPTS;
+
+static void print_help(int argc, char *argv[])
+{
+	printf("\n"
+	       "Allows for the management of AP SPI flash configuration.\n"
+	       "\n"
+	       "Usage:  " MYNAME " %s [OPTIONS] \n"
+	       "\n"
+	       "    --wp-status      \tGet the current flash WP state.\n"
+	       "    --wp-enable      \tEnable protection for the RO image section.\n"
+	       "    --wp-disable     \tDisable all write protection.\n"
+	       "    --flash-size     \tGet flash size.\n"
+	       "\n"
+	       SHARED_FLASH_ARGS_HELP,
+	       argv[0]);
+}
+
+static int do_flash(int argc, char *argv[])
+{
+	int ret = 0;
+	struct updater_config_arguments args = {0};
+	const char *prepare_ctrl_name = NULL;
+	char *servo_programmer = NULL;
+	bool enable_wp = false;
+	bool disable_wp = false;
+	bool get_wp_status = false;
+	bool get_size = false;
+
+	struct updater_config *cfg = updater_new_config();
+	assert(cfg);
+
+	opterr = 0;
+	int i;
+	while ((i = getopt_long(argc, argv, short_opts, long_opts, 0)) != -1) {
+		if (handle_flash_argument(&args, i, optarg))
+			continue;
+		switch (i) {
+		case 'h':
+			print_help(argc, argv);
+			goto out_free;
+		case 's':
+			get_wp_status = true;
+			break;
+		case 'e':
+			enable_wp = true;
+			break;
+		case 'd':
+			disable_wp = true;
+			break;
+		case 'z':
+			get_size = true;
+			break;
+		case 'v':
+			args.verbosity++;
+			break;
+		case '?':
+			ret = -1;
+			if (optopt)
+				ERROR("Unrecognized option: -%c\n", optopt);
+			else if (argv[optind - 1])
+				ERROR("Unrecognized option (possibly '%s')\n",
+				      argv[optind - 1]);
+			else
+				ERROR("Unrecognized option.\n");
+			break;
+		default:
+			ret = -1;
+			ERROR("Failed parsing options.\n");
+		}
+	}
+	if (optind < argc) {
+		ret = -1;
+		ERROR("Unexpected arguments.\n");
+	}
+
+	if (!get_size && !enable_wp && !disable_wp && !get_wp_status) {
+		print_help(argc, argv);
+		goto out_free;
+	}
+
+	if (enable_wp && disable_wp) {
+		ret = -1;
+		ERROR("--wp-enable and --wp-disable cannot be used together.\n");
+		goto out_free;
+	}
+
+	if (args.detect_servo) {
+		servo_programmer = host_detect_servo(&prepare_ctrl_name);
+
+		if (!servo_programmer) {
+			ret = -1;
+			ERROR("No servo detected.\n");
+			goto out_free;
+		}
+		if (!args.programmer)
+			args.programmer = servo_programmer;
+	}
+
+	int update_needed;
+	ret = updater_setup_config(cfg, &args, &update_needed);
+	prepare_servo_control(prepare_ctrl_name, 1);
+
+	if (!ret && get_size)
+		ret = print_flash_size(cfg);
+
+	if (!ret && enable_wp)
+		ret = set_flash_wp(cfg, true);
+
+	if (!ret && disable_wp)
+		ret = set_flash_wp(cfg, false);
+
+	if (!ret && get_wp_status)
+		ret = print_wp_status(cfg);
+
+out_free:
+	prepare_servo_control(prepare_ctrl_name, 0);
+	free(servo_programmer);
+	updater_delete_config(cfg);
+
+	return ret;
+}
+#define CMD_HELP_STR "Manage AP SPI flash properties and writeprotect configuration"
+
+#else /* USE_FLASHROM */
+
+static int do_flash(int argc, char *argv[])
+{
+	FATAL(MYNAME " was built without flashrom support, `flash` command unavailable!\n");
+	return -1;
+}
+#define CMD_HELP_STR "Manage AP SPI flash properties and writeprotect configuration (unavailable in this build)"
+
+#endif /* !USE_FLASHROM */
+
+DECLARE_FUTIL_COMMAND(flash, do_flash, VBOOT_VERSION_ALL, CMD_HELP_STR);
diff --git a/futility/updater.h b/futility/updater.h
index 3d04cb4..1b371f9 100644
--- a/futility/updater.h
+++ b/futility/updater.h
@@ -12,7 +12,8 @@
 #include "updater_utils.h"
 
 /* FMAP section names. */
-static const char * const FMAP_RO_FMAP = "FMAP",
+static const char * const FMAP_RO = "WP_RO",
+		  * const FMAP_RO_FMAP = "FMAP",
 		  * const FMAP_RO_FRID = "RO_FRID",
 		  * const FMAP_RO_SECTION = "RO_SECTION",
 		  * const FMAP_RO_CBFS = "COREBOOT",
diff --git a/host/lib/flashrom_drv.c b/host/lib/flashrom_drv.c
index 09e7f46..6eb0de8 100644
--- a/host/lib/flashrom_drv.c
+++ b/host/lib/flashrom_drv.c
@@ -311,3 +311,95 @@
 
 	return ret;
 }
+
+int flashrom_set_wp(const char *prog_with_params, bool wp_mode,
+		    uint32_t wp_start, uint32_t wp_len, int verbosity)
+{
+	int ret = 1;
+
+	g_verbose_screen = (verbosity == -1) ? FLASHROM_MSG_INFO : verbosity;
+
+	struct flashrom_programmer *prog = NULL;
+	struct flashrom_flashctx *flashctx = NULL;
+
+	struct flashrom_wp_cfg *cfg = NULL;
+
+	char *programmer, *params;
+	char *tmp = flashrom_extract_params(prog_with_params, &programmer,
+					    &params);
+
+	flashrom_set_log_callback((flashrom_log_callback *)&flashrom_print_cb);
+
+	if (flashrom_init(1)
+		|| flashrom_programmer_init(&prog, programmer, params))
+		goto err_init;
+
+	if (flashrom_flash_probe(&flashctx, prog, NULL))
+		goto err_probe;
+
+	if (flashrom_wp_cfg_new(&cfg) != FLASHROM_WP_OK)
+		goto err_cleanup;
+
+	enum flashrom_wp_mode mode = wp_mode ?
+			FLASHROM_WP_MODE_HARDWARE : FLASHROM_WP_MODE_DISABLED;
+	flashrom_wp_set_mode(cfg, mode);
+	flashrom_wp_set_range(cfg, wp_start, wp_len);
+
+	if (flashrom_wp_write_cfg(flashctx, cfg) != FLASHROM_WP_OK)
+		goto err_write_cfg;
+
+	ret = 0;
+
+err_write_cfg:
+	flashrom_wp_cfg_release(cfg);
+
+err_cleanup:
+	flashrom_flash_release(flashctx);
+
+err_probe:
+	if (flashrom_programmer_shutdown(prog))
+		ret = 1;
+
+err_init:
+	free(tmp);
+
+	return ret;
+}
+
+int flashrom_get_size(const char *prog_with_params,
+		      uint32_t *flash_len, int verbosity)
+{
+	int r = 0;
+
+	g_verbose_screen = (verbosity == -1) ? FLASHROM_MSG_INFO : verbosity;
+
+	char *programmer, *params;
+	char *tmp = flashrom_extract_params(prog_with_params,
+					    &programmer, &params);
+
+	struct flashrom_programmer *prog = NULL;
+	struct flashrom_flashctx *flashctx = NULL;
+
+	flashrom_set_log_callback((flashrom_log_callback *)&flashrom_print_cb);
+
+	if (flashrom_init(1) ||
+	    flashrom_programmer_init(&prog, programmer, params)) {
+		r = -1;
+		goto err_init;
+	}
+	if (flashrom_flash_probe(&flashctx, prog, NULL)) {
+		r = -1;
+		goto err_probe;
+	}
+
+	*flash_len = flashrom_flash_getsize(flashctx);
+
+	flashrom_flash_release(flashctx);
+
+err_probe:
+	r |= flashrom_programmer_shutdown(prog);
+
+err_init:
+	free(tmp);
+	return r;
+}
diff --git a/host/lib/include/flashrom.h b/host/lib/include/flashrom.h
index 5fb7a03..4b61d19 100644
--- a/host/lib/include/flashrom.h
+++ b/host/lib/include/flashrom.h
@@ -88,3 +88,28 @@
  */
 int flashrom_get_wp(const char *programmer, bool *wp_mode,
 		    uint32_t *wp_start, uint32_t *wp_len, int verbosity);
+
+/**
+ * Set wp state using flashrom.
+ *
+ * @param programmer	The name of the programmer to use for writing the
+ *                      writeprotect state.
+ * @param wp_mode       WP mode to set. true to enable, false disable WP.
+ * @param wp_start      WP start addr to set
+ * @param wp_len        WP region length set
+ *
+ * @return 0 on success, or a relevant error.
+ */
+int flashrom_set_wp(const char *programmer, bool wp_mode,
+		    uint32_t wp_start, uint32_t wp_len, int verbosity);
+
+/**
+ * Get flash size using flashrom.
+ *
+ * @param programmer	The name of the programmer to use.
+ * @param flash_len     Pointer to a uint32_t to store chip length.
+ *
+ * @return 0 on success, or a relevant error.
+ */
+int flashrom_get_size(const char *programmer, uint32_t *flash_len,
+		      int verbosity);
diff --git a/tests/futility/run_test_scripts.sh b/tests/futility/run_test_scripts.sh
index 662304d..2f9fbfc 100755
--- a/tests/futility/run_test_scripts.sh
+++ b/tests/futility/run_test_scripts.sh
@@ -20,6 +20,7 @@
 TESTS="
 ${SCRIPT_DIR}/futility/test_create.sh
 ${SCRIPT_DIR}/futility/test_dump_fmap.sh
+${SCRIPT_DIR}/futility/test_flash_util.sh
 ${SCRIPT_DIR}/futility/test_gbb_utility.sh
 ${SCRIPT_DIR}/futility/test_load_fmap.sh
 ${SCRIPT_DIR}/futility/test_main.sh
diff --git a/tests/futility/test_flash_util.sh b/tests/futility/test_flash_util.sh
new file mode 100755
index 0000000..e18a35c
--- /dev/null
+++ b/tests/futility/test_flash_util.sh
@@ -0,0 +1,26 @@
+#!/bin/bash -eux
+# Copyright 2023 The ChromiumOS Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+me=${0##*/}
+TMP="${me}.tmp"
+
+# Work in scratch directory
+cd "${OUTDIR}"
+
+# 8MB test image
+TEST_BIOS="${SCRIPT_DIR}/futility/data/bios_link_mp.bin"
+TEST_PROG="dummy:image=${TEST_BIOS},emulate=VARIABLE_SIZE,size=8388608"
+
+# Test flash size
+flash_size=$("${FUTILITY}" flash --flash-size -p "${TEST_PROG}")
+[ "${flash_size}" = "Flash size: 0x00800000" ]
+
+# Test WP status (VARIABLE_SIZE always has WP disabled)
+wp_status=$("${FUTILITY}" flash --wp-status -p "${TEST_PROG}")
+[ "${wp_status}" = "WP status: disabled" ]
+
+# Cleanup
+rm -f "${TMP}"*
+exit 0