Provide a way to disable counting failed boots

When the lid is closed and external power is applied
the system may boot and shut down faster than required
for the OS to determine that things were alright.

In timed charging setups this led to systems ending up
to consider the current version broken because it "failed"
repeatedly.

Remain generic about the reason for not counting boots
since there may be more situations in which we want to
handle the situation optimistically.

BRANCH=none
BUG=chromium:446945
TEST=none

Change-Id: Iea350e3c98d5c00156da682e52c90a882ba017c0
Signed-off-by: Patrick Georgi <pgeorgi@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/249150
Reviewed-by: Randall Spangler <rspangler@chromium.org>
diff --git a/firmware/2lib/2misc.c b/firmware/2lib/2misc.c
index 3188473..360365d 100644
--- a/firmware/2lib/2misc.c
+++ b/firmware/2lib/2misc.c
@@ -360,8 +360,9 @@
 		/* Still trying this firmware */
 		vb2_nv_set(ctx, VB2_NV_FW_RESULT, VB2_FW_RESULT_TRYING);
 
-		/* Decrement non-zero try count */
-		vb2_nv_set(ctx, VB2_NV_TRY_COUNT, tries - 1);
+		/* Decrement non-zero try count, unless told not to */
+		if (!(ctx->flags & VB2_CONTEXT_NOFAIL_BOOT))
+			vb2_nv_set(ctx, VB2_NV_TRY_COUNT, tries - 1);
 	}
 
 	/* Store the slot we're trying */
diff --git a/firmware/2lib/include/2api.h b/firmware/2lib/include/2api.h
index 1341528..3fbd7ed 100644
--- a/firmware/2lib/include/2api.h
+++ b/firmware/2lib/include/2api.h
@@ -100,6 +100,9 @@
 	 * back to its underlying storage, then may clear this flag.
 	 */
 	VB2_CONTEXT_SECDATAK_CHANGED = (1 << 11),
+
+	/* Boot optimistically: don't touch failure counters */
+	VB2_CONTEXT_NOFAIL_BOOT = (1 << 12),
 };
 
 /*
diff --git a/firmware/include/vboot_api.h b/firmware/include/vboot_api.h
index e66a2fb..397d884 100644
--- a/firmware/include/vboot_api.h
+++ b/firmware/include/vboot_api.h
@@ -229,9 +229,12 @@
 #define VB_INIT_FLAG_VIRTUAL_REC_SWITCH  0x00001000
 /* Set when we are calling VbInit() before loading Option ROMs */
 #define VB_INIT_FLAG_BEFORE_OPROM_LOAD   0x00002000
-
 /* Allow USB boot on transition to dev */
 #define VB_INIT_FLAG_ALLOW_USB_BOOT	 0x00004000
+/* Set when we can't reliably identify boot failures. This prevents
+ * the boot-try counters from decrementing.
+ */
+#define VB_INIT_FLAG_NOFAIL_BOOT         0x00008000
 
 /*
  * Output flags for VbInitParams.out_flags.  Used to indicate potential boot
diff --git a/firmware/include/vboot_struct.h b/firmware/include/vboot_struct.h
index 09abf13..3a3f534 100644
--- a/firmware/include/vboot_struct.h
+++ b/firmware/include/vboot_struct.h
@@ -322,6 +322,8 @@
 #define VBSD_OPROM_MATTERS               0x00010000
 /* Firmware has loaded the VGA Option ROM */
 #define VBSD_OPROM_LOADED                0x00020000
+/* Don't try for boot failures */
+#define VBSD_NOFAIL_BOOT                 0x00040000
 
 /*
  * Supported flags by header version.  It's ok to add new flags while keeping
diff --git a/firmware/lib/vboot_api_init.c b/firmware/lib/vboot_api_init.c
index 4371042..10b435c 100644
--- a/firmware/lib/vboot_api_init.c
+++ b/firmware/lib/vboot_api_init.c
@@ -76,6 +76,8 @@
 		shared->flags |= VBSD_BOOT_S3_RESUME;
 	if (iparams->flags & VB_INIT_FLAG_RO_NORMAL_SUPPORT)
 		shared->flags |= VBSD_BOOT_RO_NORMAL_SUPPORT;
+	if (iparams->flags & VB_INIT_FLAG_NOFAIL_BOOT)
+		shared->flags |= VBSD_NOFAIL_BOOT;
 	if (iparams->flags & VB_INIT_FLAG_EC_SOFTWARE_SYNC)
 		shared->flags |= VBSD_EC_SOFTWARE_SYNC;
 	if (iparams->flags & VB_INIT_FLAG_EC_SLOW_UPDATE)
diff --git a/firmware/lib/vboot_firmware.c b/firmware/lib/vboot_firmware.c
index e6a31fa..7e6010b 100644
--- a/firmware/lib/vboot_firmware.c
+++ b/firmware/lib/vboot_firmware.c
@@ -78,7 +78,8 @@
 	/* Read try-b count and decrement if necessary */
 	VbNvGet(vnc, VBNV_TRY_B_COUNT, &try_b_count);
 	if (0 != try_b_count) {
-		VbNvSet(vnc, VBNV_TRY_B_COUNT, try_b_count - 1);
+		if (!(shared->flags & VBSD_NOFAIL_BOOT))
+			VbNvSet(vnc, VBNV_TRY_B_COUNT, try_b_count - 1);
 		shared->flags |= VBSD_FWB_TRIED;
 	}
 
diff --git a/firmware/lib/vboot_kernel.c b/firmware/lib/vboot_kernel.c
index 86cf6ef..c6d4e27 100644
--- a/firmware/lib/vboot_kernel.c
+++ b/firmware/lib/vboot_kernel.c
@@ -441,8 +441,12 @@
 		if (VbKernelHasFlags(preamble) == VBOOT_SUCCESS)
 			params->flags = preamble->flags;
 
-		/* Update GPT to note this is the kernel we're trying */
-		GptUpdateKernelEntry(&gpt, GPT_UPDATE_ENTRY_TRY);
+		/* Update GPT to note this is the kernel we're trying.
+		 * But not when we assume that the boot process may
+		 * not complete for valid reasons (eg. early shutdown).
+		 */
+		if (!(shared->flags & VBSD_NOFAIL_BOOT))
+			GptUpdateKernelEntry(&gpt, GPT_UPDATE_ENTRY_TRY);
 
 		/*
 		 * If we're in recovery mode or we're about to boot a
diff --git a/tests/vb2_misc_tests.c b/tests/vb2_misc_tests.c
index 4d80450..e41a16d 100644
--- a/tests/vb2_misc_tests.c
+++ b/tests/vb2_misc_tests.c
@@ -446,6 +446,17 @@
 	TEST_EQ(sd->fw_slot, 1, "selected B");
 	TEST_NEQ(cc.flags & VB2_CONTEXT_FW_SLOT_B, 0, "ctx says choose B");
 
+	/* Slot A ran out of tries, even with nofail active */
+	reset_common_data();
+	cc.flags |= VB2_CONTEXT_NOFAIL_BOOT;
+	vb2_nv_set(&cc, VB2_NV_FW_RESULT, VB2_FW_RESULT_TRYING);
+	TEST_SUCC(vb2_select_fw_slot(&cc), "select slot A out of tries");
+	TEST_EQ(vb2_nv_get(&cc, VB2_NV_TRY_NEXT), 1, "try B next");
+	TEST_NEQ(sd->status & VB2_SD_STATUS_CHOSE_SLOT, 0, "chose slot");
+	TEST_EQ(vb2_nv_get(&cc, VB2_NV_FW_TRIED), 1, "tried B");
+	TEST_EQ(sd->fw_slot, 1, "selected B");
+	TEST_NEQ(cc.flags & VB2_CONTEXT_FW_SLOT_B, 0, "ctx says choose B");
+
 	/* Slot A used up a try */
 	reset_common_data();
 	vb2_nv_set(&cc, VB2_NV_TRY_COUNT, 3);
@@ -458,6 +469,19 @@
 	TEST_EQ(cc.flags & VB2_CONTEXT_FW_SLOT_B, 0, "didn't choose B");
 	TEST_EQ(vb2_nv_get(&cc, VB2_NV_TRY_COUNT), 2, "tries decremented");
 
+	/* Slot A failed, but nofail active */
+	reset_common_data();
+	cc.flags |= VB2_CONTEXT_NOFAIL_BOOT;
+	vb2_nv_set(&cc, VB2_NV_TRY_COUNT, 3);
+	TEST_SUCC(vb2_select_fw_slot(&cc), "try slot A");
+	TEST_EQ(vb2_nv_get(&cc, VB2_NV_FW_RESULT),
+		VB2_FW_RESULT_TRYING, "result trying");
+	TEST_NEQ(sd->status & VB2_SD_STATUS_CHOSE_SLOT, 0, "chose slot");
+	TEST_EQ(vb2_nv_get(&cc, VB2_NV_FW_TRIED), 0, "tried A");
+	TEST_EQ(sd->fw_slot, 0, "selected A");
+	TEST_EQ(cc.flags & VB2_CONTEXT_FW_SLOT_B, 0, "didn't choose B");
+	TEST_EQ(vb2_nv_get(&cc, VB2_NV_TRY_COUNT), 3, "tries not decremented");
+
 	/* Tried/result get copied to the previous fields */
 	reset_common_data();
 	vb2_nv_set(&cc, VB2_NV_FW_TRIED, 0);
diff --git a/tests/vboot_firmware_tests.c b/tests/vboot_firmware_tests.c
index 651197c..f83d970 100644
--- a/tests/vboot_firmware_tests.c
+++ b/tests/vboot_firmware_tests.c
@@ -371,6 +371,39 @@
   VbNvGet(&vnc, VBNV_TRY_B_COUNT, &u);
   TEST_EQ(u, 4, "Used up a try");
 
+  /* There's a optimistic boot mode that doesn't consume tries.
+   * The behaviour should differ only in that the try count doesn't change. */
+  /* Optimistic boot case 1: count == 0: Go for A */
+  ResetMocks();
+  mpreamble[0].flags = VB_FIRMWARE_PREAMBLE_USE_RO_NORMAL;
+  mpreamble[1].flags = VB_FIRMWARE_PREAMBLE_USE_RO_NORMAL;
+  shared->flags |= VBSD_BOOT_RO_NORMAL_SUPPORT;
+  shared->flags |= VBSD_NOFAIL_BOOT;
+  VbNvSet(&vnc, VBNV_TRY_B_COUNT, 0);
+  TestLoadFirmware(VBERROR_SUCCESS, 0, "Give up on B");
+  TEST_EQ(shared->check_fw_a_result, VBSD_LF_CHECK_VALID, "RO normal A valid");
+  TEST_EQ(shared->check_fw_b_result, 0, "RO normal B not checked");
+  TEST_EQ(shared->firmware_index, 0, "Boot A");
+  TEST_EQ(shared->flags & VBSD_FWB_TRIED, 0, "Didn't try firmware B");
+  TEST_EQ(shared->kernel_subkey.algorithm, 7, "Copy kernel subkey");
+  VbNvGet(&vnc, VBNV_TRY_B_COUNT, &u);
+  TEST_EQ(u, 0, "try count still zero");
+  /* Optimistic boot case 2: count > 0: count unchanged, use B */
+  ResetMocks();
+  mpreamble[0].flags = VB_FIRMWARE_PREAMBLE_USE_RO_NORMAL;
+  mpreamble[1].flags = VB_FIRMWARE_PREAMBLE_USE_RO_NORMAL;
+  shared->flags |= VBSD_BOOT_RO_NORMAL_SUPPORT;
+  shared->flags |= VBSD_NOFAIL_BOOT;
+  VbNvSet(&vnc, VBNV_TRY_B_COUNT, 5);
+  TestLoadFirmware(VBERROR_SUCCESS, 0, "Check B then A");
+  TEST_EQ(shared->check_fw_a_result, 0, "RO normal A not checked ");
+  TEST_EQ(shared->check_fw_b_result, VBSD_LF_CHECK_VALID, "RO normal B valid");
+  TEST_EQ(shared->firmware_index, 1, "Boot B");
+  TEST_NEQ(shared->flags & VBSD_FWB_TRIED, 0, "Tried firmware B");
+  TEST_EQ(shared->kernel_subkey.algorithm, 8, "Copy kernel subkey");
+  VbNvGet(&vnc, VBNV_TRY_B_COUNT, &u);
+  TEST_EQ(u, 5, "Not used up a try");
+
   /* If both A and B are valid and grater version than TPM, A is
    * selected and B preamble (but not body) is verified. */
   ResetMocks();