futility: updater: Enable ec_partial_recovery quirk by default

Enable QUIRK_EC_PARTIAL_RECOVERY (ec_partial_recovery=1) by default so
full/recovery EC firmware updates only flash WP_RO when the EC is active
in RW with EC software sync enabled, leaving EC_RW to be updated by EC
software sync on the next reboot.

During a full EC update (EC_RECOVERY_FULL, RO+RW), flashrom's
cros_ec_prepare() sees EC_IMAGE_RO | EC_IMAGE_RW and forces an
unconditional sysjump from RW to RO when EXEC_IN_RAM is unsupported (such
as on ITE IT8xxx2 ECs), resetting the EC/TCPC or dropping host
communication while the AP is in S0 (b/574563907). Flashrom commit
af7ed034 (b/278652302) added support for RO-only updates (-i WP_RO,
EC_RECOVERY_RO) to skip the sysjump when the EC is already running in RW,
allowing WP_RO to be updated in place while EC software sync updates
EC_RW on reboot.

Callers or board CBFS configs that require an immediate full RO+RW write
can still opt out via --quirks=ec_partial_recovery=0.

BRANCH=none
BUG=b:574563907
TEST=make runfutiltests
TEST=futility update -a /usr/sbin/chromeos-firmwareupdate --mode=recovery
     --force on dirks (IT81302) updates WP_RO without crashing EC in RW

Change-Id: Iadcb674d804e3397075fe93462199424ffa1a564
Signed-off-by: Jon Murphy <jpmurphy@google.com>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/8609747
Reviewed-by: Grzegorz Bernacki <bernacki@google.com>
diff --git a/futility/updater_dut.c b/futility/updater_dut.c
index 4946201..f1e950f 100644
--- a/futility/updater_dut.c
+++ b/futility/updater_dut.c
@@ -42,6 +42,7 @@
 	return VbSetSystemPropertyString(key, value);
 }
 
+test_mockable
 int dut_get_property_string(const char *key, char *dest, size_t size,
 			    struct updater_config *cfg)
 {
@@ -62,6 +63,7 @@
 	return VbSetSystemPropertyInt(key, value);
 }
 
+test_mockable
 int dut_get_property_int(const char *key, struct updater_config *cfg)
 {
 	if (cfg->dut_is_remote) {
diff --git a/futility/updater_quirks.c b/futility/updater_quirks.c
index ed99104..36db084 100644
--- a/futility/updater_quirks.c
+++ b/futility/updater_quirks.c
@@ -232,14 +232,13 @@
  *
  * Some EC will reset TCPC when doing sysjump, and will make rootfs unavailable
  * if the system was boot from USB, or other unexpected issues even if the
- * system was boot from internal disk. To prevent that, try to partial update
- * only RO and expect EC software sync to update RW later, or perform EC RO
- * software sync.
- *
- * Note: EC RO software sync was not fully tested and may cause problems
- *       (b/218612817, b/187789991).
- *       RO-update (without extra sysjump) needs support from flashrom and is
- *       currently disabled.
+ * system was boot from internal disk. In addition, full RO+RW updates force an
+ * unconditional sysjump from RW to RO in flashrom's cros_ec_prepare(), which
+ * resets XIP ECs (such as ITE IT8xxx2) or drops host communication while the
+ * AP is in S0 (b/574563907). With flashrom support for RO-only updates without
+ * sysjump when already in RW (b/278652302), partial update only WP_RO when EC
+ * is active in RW with software sync enabled, and expect EC software sync to
+ * update RW on the next boot.
  *
  * Returns:
  *  EC_RECOVERY_FULL to indicate a full recovery is needed.
@@ -426,6 +425,7 @@
 	quirks->name = "ec_partial_recovery";
 	quirks->help = "chromium/1024401; recover EC by partial RO update.";
 	quirks->apply = quirk_ec_partial_recovery;
+	quirks->value = 1;
 
 	quirks = &cfg->quirks[QUIRK_OVERRIDE_CUSTOM_LABEL];
 	quirks->name = "override_custom_label";
diff --git a/tests/futility/test_updater_utils.c b/tests/futility/test_updater_utils.c
index 2eec2df..58a095e 100644
--- a/tests/futility/test_updater_utils.c
+++ b/tests/futility/test_updater_utils.c
@@ -5,6 +5,8 @@
 
 #include "unit_tests.h"
 #include "updater_utils.c"
+#include "crossystem.h"
+#include "vboot_struct.h"
 
 #define IMAGE_MAIN	 GET_WORK_COPY_TEST_DATA_FILE_PATH("image.bin")
 #define ARCHIVE		 GET_WORK_COPY_TEST_DATA_FILE_PATH("images.zip")
@@ -613,6 +615,97 @@
 	UNIT_TEST_RETURN;
 }
 
+static bool mock_dut_props;
+static int mock_vdat_flags;
+static const char *mock_ecfw_act;
+
+int dut_get_property_int(const char *key, struct updater_config *cfg)
+{
+	if (cfg->dut_is_remote)
+		return -1;
+	if (mock_dut_props) {
+		if (strcmp(key, "vdat_flags") == 0)
+			return mock_vdat_flags;
+		return -1;
+	}
+	return VbGetSystemPropertyInt(key);
+}
+
+int dut_get_property_string(const char *key, char *dest, size_t size,
+			    struct updater_config *cfg)
+{
+	if (cfg->dut_is_remote)
+		return -1;
+	if (mock_dut_props) {
+		if (strcmp(key, "ecfw_act") == 0 && mock_ecfw_act) {
+			strncpy(dest, mock_ecfw_act, size);
+			dest[size - 1] = '\0';
+			return 0;
+		}
+		return -1;
+	}
+	return VbGetSystemPropertyString(key, dest, size);
+}
+
+static enum unit_result test_ec_partial_recovery(void)
+{
+	UNIT_TEST_BEGIN;
+	struct updater_config *cfg = updater_new_config();
+	struct vb2_gbb_header *gbb = NULL; /* Do not free. */
+	FmapAreaHeader *ah = NULL;	   /* Do not free. */
+
+	mock_dut_props = true;
+	UNIT_ASSERT(cfg != NULL);
+	TEST_EQ(get_config_quirk(QUIRK_EC_PARTIAL_RECOVERY, cfg), 1,
+		"Default ec_partial_recovery quirk is enabled");
+
+	UNIT_ASSERT(load_firmware_image(&cfg->image, IMAGE_MAIN, NULL) == 0);
+	UNIT_ASSERT(load_firmware_image(&cfg->ec_image, IMAGE_MAIN, NULL) == 0);
+
+	mock_vdat_flags = VBSD_EC_SOFTWARE_SYNC;
+	mock_ecfw_act = "RW";
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_RO,
+		"EC partial recovery: EC in RW with software sync enabled -> WP_RO only");
+
+	mock_ecfw_act = "RO";
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_FULL,
+		"EC partial recovery: EC in RO -> full update");
+
+	mock_ecfw_act = "RW";
+	mock_vdat_flags = 0;
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_FULL,
+		"EC partial recovery: software sync not enabled -> full update");
+
+	mock_vdat_flags = -1;
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_FULL,
+		"EC partial recovery: unknown vdat_flags -> full update");
+
+	mock_vdat_flags = VBSD_EC_SOFTWARE_SYNC;
+	gbb = (struct vb2_gbb_header *)find_gbb(&cfg->image);
+	UNIT_ASSERT(gbb != NULL);
+	gbb->flags |= VB2_GBB_FLAG_DISABLE_EC_SOFTWARE_SYNC;
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_FULL,
+		"EC partial recovery: GBB disables software sync -> full update");
+	gbb->flags &= ~VB2_GBB_FLAG_DISABLE_EC_SOFTWARE_SYNC;
+
+	cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].value = 0;
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_FULL,
+		"EC partial recovery: quirk disabled (=0) -> full update");
+	cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].value = 1;
+
+	UNIT_ASSERT(fmap_find_by_name(cfg->ec_image.data, cfg->ec_image.size,
+				      cfg->ec_image.fmap_header, "WP_RO", &ah) != NULL);
+	ah->area_name[0] = '\0';
+	TEST_EQ(cfg->quirks[QUIRK_EC_PARTIAL_RECOVERY].apply(cfg), EC_RECOVERY_FULL,
+		"EC partial recovery: missing WP_RO section -> full update");
+
+unit_cleanup:
+	mock_dut_props = false;
+	if (cfg)
+		updater_delete_config(cfg);
+	UNIT_TEST_RETURN;
+}
+
 int main(int argc, char *argv[])
 {
 	if (prepare_test_data() == UNIT_FAIL) {
@@ -631,6 +724,7 @@
 	test_preserve_firmware_section();
 	test_gbb();
 	test_misc();
+	test_ec_partial_recovery();
 
 	return !gTestSuccess;
 }