vboot: Introduce alternate boot functionality

Introduce alternate boot functionality both via keyboard shortcut
("Ctrl+L") to directly boot into the default alternate bootloader,
and via menu ("Alternate bootloader" on dev screen) to show a screen
listing available bootloaders.

BUG=b:146399181, b:161092974
TEST=make clean && make runtests
BRANCH=puff, zork

Cq-Depend: chromium:2339040
Signed-off-by: Joel Kitching <kitching@google.com>
Change-Id: I28f157936017719dc95656db147967f5e61a1407
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/vboot_reference/+/2335017
Commit-Queue: Yu-Ping Wu <yupingso@chromium.org>
Tested-by: Hsuan Ting Chen <roccochen@chromium.org>
Reviewed-by: Yu-Ping Wu <yupingso@chromium.org>
Reviewed-by: Joel Kitching <kitching@chromium.org>
diff --git a/firmware/2lib/2ui.c b/firmware/2lib/2ui.c
index d80002d..4ddbcef 100644
--- a/firmware/2lib/2ui.c
+++ b/firmware/2lib/2ui.c
@@ -410,6 +410,8 @@
 	if (ui->key == VB_KEY_CTRL('D') ||
 	    (DETACHABLE && ui->key == VB_BUTTON_VOL_DOWN_LONG_PRESS))
 		return vb2_ui_developer_mode_boot_internal_action(ui);
+	if (ui->key == VB_KEY_CTRL('L'))
+		return vb2_ui_developer_mode_boot_alternate_action(ui);
 	if (ui->key == '\t')
 		return vb2_ui_screen_change(ui, VB2_SCREEN_DEBUG_INFO);
 
diff --git a/firmware/2lib/2ui_screens.c b/firmware/2lib/2ui_screens.c
index 2e5dbfa..e1b02d0 100644
--- a/firmware/2lib/2ui_screens.c
+++ b/firmware/2lib/2ui_screens.c
@@ -642,6 +642,7 @@
 #define DEVELOPER_MODE_ITEM_RETURN_TO_SECURE 1
 #define DEVELOPER_MODE_ITEM_BOOT_INTERNAL 2
 #define DEVELOPER_MODE_ITEM_BOOT_EXTERNAL 3
+#define DEVELOPER_MODE_ITEM_SELECT_BOOTLOADER 4
 
 vb2_error_t developer_mode_init(struct vb2_ui_context *ui)
 {
@@ -662,11 +663,20 @@
 		ui->state->disabled_item_mask |=
 			1 << DEVELOPER_MODE_ITEM_BOOT_EXTERNAL;
 
+	/* Don't show "Select alternate bootloader" button if not allowed. */
+	if (!vb2_dev_boot_legacy_allowed(ui->ctx))
+		ui->state->disabled_item_mask |=
+			1 << DEVELOPER_MODE_ITEM_SELECT_BOOTLOADER;
+
 	/* Choose the default selection. */
 	switch (default_boot) {
 	case VB2_DEV_DEFAULT_BOOT_TARGET_EXTERNAL:
 		ui->state->selected_item = DEVELOPER_MODE_ITEM_BOOT_EXTERNAL;
 		break;
+	case VB2_DEV_DEFAULT_BOOT_TARGET_LEGACY:
+		ui->state->selected_item =
+			DEVELOPER_MODE_ITEM_SELECT_BOOTLOADER;
+		break;
 	default:
 		ui->state->selected_item = DEVELOPER_MODE_ITEM_BOOT_INTERNAL;
 		break;
@@ -781,6 +791,10 @@
 		.text = "Boot from external disk",
 		.action = vb2_ui_developer_mode_boot_external_action,
 	},
+	[DEVELOPER_MODE_ITEM_SELECT_BOOTLOADER] = {
+		.text = "Select alternate bootloader",
+		.target = VB2_SCREEN_DEVELOPER_SELECT_BOOTLOADER,
+	},
 	ADVANCED_OPTIONS_ITEM,
 	POWER_OFF_ITEM,
 };
@@ -860,6 +874,108 @@
 };
 
 /******************************************************************************/
+/* VB2_SCREEN_DEVELOPER_SELECT_BOOTLOADER */
+
+const struct vb2_menu_item developer_select_bootloader_items_before[] = {
+	LANGUAGE_SELECT_ITEM,
+};
+
+const struct vb2_menu_item developer_select_bootloader_items_after[] = {
+	BACK_ITEM,
+	POWER_OFF_ITEM,
+};
+
+vb2_error_t vb2_ui_developer_mode_boot_alternate_action(
+	struct vb2_ui_context *ui)
+{
+	uint32_t altfw_num;
+	const size_t menu_before_len =
+		ARRAY_SIZE(developer_select_bootloader_items_before);
+
+	if (!(ui->ctx->flags & VB2_CONTEXT_DEVELOPER_MODE) ||
+	    !vb2_dev_boot_allowed(ui->ctx) ||
+	    !vb2_dev_boot_legacy_allowed(ui->ctx)) {
+		VB2_DEBUG("ERROR: Dev mode alternate bootloader not allowed\n");
+		ui->error_beep = 1;
+		return VB2_REQUEST_UI_CONTINUE;
+	}
+
+	if (vb2ex_get_bootloader_count() == 0) {
+		VB2_DEBUG("ERROR: No bootloader was found\n");
+		ui->error_beep = 1;
+		return VB2_REQUEST_UI_CONTINUE;
+	}
+
+	if (ui->key == VB_KEY_CTRL('L')) {
+		altfw_num = 0;
+		VB2_DEBUG("Try booting from default bootloader\n");
+	} else {
+		altfw_num = ui->state->selected_item - menu_before_len + 1;
+		VB2_DEBUG("Try booting from bootloader #%u\n", altfw_num);
+	}
+
+	/* VbExLegacy will not return if successful */
+	VbExLegacy(altfw_num);
+
+	VB2_DEBUG("ERROR: Alternate bootloader failed\n");
+	/* TODO(b/161092974): Leverage the error dialog on error. */
+	ui->error_beep = 1;
+	return VB2_REQUEST_UI_CONTINUE;
+}
+
+static const struct vb2_menu *get_bootloader_menu(struct vb2_ui_context *ui)
+{
+	int i;
+	uint32_t num_bootloaders, num_items;
+	struct vb2_menu_item *items;
+	const size_t menu_before_len =
+		ARRAY_SIZE(developer_select_bootloader_items_before);
+	const size_t menu_after_len =
+		ARRAY_SIZE(developer_select_bootloader_items_after);
+
+	if (ui->bootloader_menu.num_items > 0)
+		return &ui->bootloader_menu;
+
+	/* TODO(b/161092974): Show error dialog if no bootloader. */
+	num_bootloaders = vb2ex_get_bootloader_count();
+	VB2_DEBUG("num_bootloaders: %u\n", num_bootloaders);
+	num_items = num_bootloaders + menu_before_len + menu_after_len;
+	items = malloc(num_items * sizeof(struct vb2_menu_item));
+	if (!items) {
+		VB2_DEBUG("ERROR: malloc failed for bootloader items\n");
+		return NULL;
+	}
+
+	/* Copy prefix items to the begin. */
+	memcpy(&items[0],
+	       developer_select_bootloader_items_before,
+	       menu_before_len * sizeof(struct vb2_menu_item));
+
+	/* Copy bootloaders. */
+	for (i = 0; i < num_bootloaders; i++) {
+		items[i + menu_before_len].text = "Some bootloader";
+		items[i + menu_before_len].action =
+			vb2_ui_developer_mode_boot_alternate_action;
+	}
+
+	/* Copy postfix items to the end. */
+	memcpy(&items[num_items - menu_after_len],
+	       developer_select_bootloader_items_after,
+	       menu_after_len * sizeof(struct vb2_menu_item));
+
+	ui->bootloader_menu.num_items = num_items;
+	ui->bootloader_menu.items = items;
+
+	return &ui->bootloader_menu;
+}
+
+static const struct vb2_screen_info developer_select_bootloader_screen = {
+	.id = VB2_SCREEN_DEVELOPER_SELECT_BOOTLOADER,
+	.name = "Select alternate bootloader",
+	.get_menu = get_bootloader_menu,
+};
+
+/******************************************************************************/
 /* VB2_SCREEN_DIAGNOSTICS */
 
 static const struct vb2_menu_item diagnostics_items[] = {
@@ -1071,6 +1187,7 @@
 	&developer_to_norm_screen,
 	&developer_boot_external_screen,
 	&developer_invalid_disk_screen,
+	&developer_select_bootloader_screen,
 	&diagnostics_screen,
 	&diagnostics_storage_screen,
 	&diagnostics_memory_quick_screen,
diff --git a/firmware/2lib/include/2api.h b/firmware/2lib/include/2api.h
index ee3bb53..56c0b80 100644
--- a/firmware/2lib/include/2api.h
+++ b/firmware/2lib/include/2api.h
@@ -1316,6 +1316,8 @@
 	VB2_SCREEN_DEVELOPER_BOOT_EXTERNAL	= 0x320,
 	/* Invalid external disk inserted */
 	VB2_SCREEN_DEVELOPER_INVALID_DISK	= 0x330,
+	/* Select alternate bootloader ("legacy boot") */
+	VB2_SCREEN_DEVELOPER_SELECT_BOOTLOADER	= 0x340,
 	/* Diagnostic tools */
 	VB2_SCREEN_DIAGNOSTICS			= 0x400,
 	/* Storage diagnostic screen */
@@ -1380,6 +1382,13 @@
 uint32_t vb2ex_get_locale_count(void);
 
 /**
+ * Return the number of available alternate bootloaders.
+ *
+ * @returns Number of alternate bootloaders.  0 if none or on error.
+ */
+uint32_t vb2ex_get_bootloader_count(void);
+
+/**
  * Delay for at least the specified number of milliseconds.
  *
  * @param msec			Duration in milliseconds.
diff --git a/firmware/2lib/include/2ui.h b/firmware/2lib/include/2ui.h
index cc21f58..2fefaf3 100644
--- a/firmware/2lib/include/2ui.h
+++ b/firmware/2lib/include/2ui.h
@@ -103,6 +103,9 @@
 	/* For language selection screen. */
 	struct vb2_menu language_menu;
 
+	/* For bootloader selection screen. */
+	struct vb2_menu bootloader_menu;
+
 	/* For error beep sound. */
 	int error_beep;
 
@@ -118,6 +121,8 @@
 	struct vb2_ui_context *ui);
 vb2_error_t vb2_ui_developer_mode_boot_external_action(
 	struct vb2_ui_context *ui);
+vb2_error_t vb2_ui_developer_mode_boot_alternate_action(
+	struct vb2_ui_context *ui);
 
 /**
  * Get info struct of a screen.
diff --git a/firmware/stub/vboot_api_stub.c b/firmware/stub/vboot_api_stub.c
index e7ee136..5678521 100644
--- a/firmware/stub/vboot_api_stub.c
+++ b/firmware/stub/vboot_api_stub.c
@@ -49,9 +49,14 @@
 	return 0;
 }
 
-vb2_error_t VbExGetAltFwIdxMask(void)
+uint32_t vb2ex_get_bootloader_count(void)
 {
-	return VB2_SUCCESS;
+	return 0;
+}
+
+uint32_t VbExGetAltFwIdxMask(void)
+{
+	return 0;
 }
 
 uint32_t VbExKeyboardRead(void)
diff --git a/tests/vb2_ui_action_tests.c b/tests/vb2_ui_action_tests.c
index 493162f..79f82ca 100644
--- a/tests/vb2_ui_action_tests.c
+++ b/tests/vb2_ui_action_tests.c
@@ -59,6 +59,13 @@
 static vb2_error_t mock_vbtlk_retval;
 static uint32_t mock_vbtlk_expected_flag;
 
+static int mock_dev_boot_allowed;
+static int mock_dev_boot_legacy_allowed;
+
+static int mock_vbexlegacy_called;
+static enum VbAltFwIndex_t mock_altfw_num_last;
+static uint32_t mock_bootloader_count;
+
 /* Mock actions */
 static uint32_t mock_action_called;
 static uint32_t mock_action_countdown_limit;
@@ -334,6 +341,15 @@
 	/* For VbTryLoadKernel */
 	mock_vbtlk_retval = VB2_ERROR_MOCK;
 	mock_vbtlk_expected_flag = MOCK_IGNORE;
+
+	/* For dev_boot* in 2misc.h */
+	mock_dev_boot_allowed = 1;
+	mock_dev_boot_legacy_allowed = 0;
+
+	/* For VbExLegacy */
+	mock_vbexlegacy_called = 0;
+	mock_altfw_num_last = -100;
+	mock_bootloader_count = 2;
 }
 
 /* Mock functions */
@@ -445,6 +461,32 @@
 	return mock_vbtlk_retval;
 }
 
+int vb2_dev_boot_allowed(struct vb2_context *c)
+{
+	return mock_dev_boot_allowed;
+}
+
+int vb2_dev_boot_legacy_allowed(struct vb2_context *c)
+{
+	return mock_dev_boot_legacy_allowed;
+}
+
+vb2_error_t VbExLegacy(enum VbAltFwIndex_t altfw_num)
+{
+	mock_vbexlegacy_called++;
+	mock_altfw_num_last = altfw_num;
+
+	if (altfw_num <= mock_bootloader_count)
+		return VB2_SUCCESS;
+	else
+		return VB2_ERROR_UNKNOWN;
+}
+
+uint32_t vb2ex_get_bootloader_count(void)
+{
+	return mock_bootloader_count;
+}
+
 /* Tests */
 static void menu_prev_tests(void)
 {
@@ -630,6 +672,58 @@
 	VB2_DEBUG("...done.\n");
 }
 
+static void vb2_ui_developer_mode_boot_alternate_action_tests(void)
+{
+	VB2_DEBUG("Test developer mode boot alternate action...\n");
+
+	/* Not allowed: not in dev mode */
+	reset_common_data();
+	mock_dev_boot_legacy_allowed = 1;
+	TEST_EQ(vb2_ui_developer_mode_boot_alternate_action(&mock_ui_context),
+		VB2_REQUEST_UI_CONTINUE, "not allowed: not in dev mode");
+	TEST_EQ(mock_vbexlegacy_called, 0, "  VbExLegacy not called");
+
+	/* Not allowed: dev boot not allowed */
+	reset_common_data();
+	ctx->flags |= VB2_CONTEXT_DEVELOPER_MODE;
+	mock_dev_boot_allowed = 0;
+	mock_dev_boot_legacy_allowed = 1;
+	TEST_EQ(vb2_ui_developer_mode_boot_alternate_action(&mock_ui_context),
+		VB2_REQUEST_UI_CONTINUE, "not allowed: dev boot not allowed");
+	TEST_EQ(mock_vbexlegacy_called, 0, "  VbExLegacy not called");
+
+	/* Not allowed: boot legacy not allowed */
+	reset_common_data();
+	ctx->flags |= VB2_CONTEXT_DEVELOPER_MODE;
+	TEST_EQ(vb2_ui_developer_mode_boot_alternate_action(&mock_ui_context),
+		VB2_REQUEST_UI_CONTINUE,
+		"not allowed: boot legacy not allowed");
+	TEST_EQ(mock_vbexlegacy_called, 0, "  VbExLegacy not called");
+
+	/* Allowed */
+	reset_common_data();
+	ctx->flags |= VB2_CONTEXT_DEVELOPER_MODE;
+	mock_dev_boot_legacy_allowed = 1;
+	mock_ui_context.state->selected_item = 2;
+	TEST_EQ(vb2_ui_developer_mode_boot_alternate_action(&mock_ui_context),
+		VB2_REQUEST_UI_CONTINUE, "allowed");
+	TEST_EQ(mock_vbexlegacy_called, 1, "  VbExLegacy called once");
+	TEST_EQ(mock_altfw_num_last, 2, "  select bootloader #2");
+
+	/* CTRL+L = default bootloader */
+	reset_common_data();
+	ctx->flags |= VB2_CONTEXT_DEVELOPER_MODE;
+	mock_dev_boot_legacy_allowed = 1;
+	mock_ui_context.key = VB_KEY_CTRL('L');
+	mock_ui_context.state->selected_item = 4;  /* Ignored */
+	TEST_EQ(vb2_ui_developer_mode_boot_alternate_action(&mock_ui_context),
+		VB2_REQUEST_UI_CONTINUE, "allowed: ctrl+l");
+	TEST_EQ(mock_vbexlegacy_called, 1, "  VbExLegacy called once");
+	TEST_EQ(mock_altfw_num_last, 0, "  select bootloader #0");
+
+	VB2_DEBUG("...done.\n");
+}
+
 static void manual_recovery_action_tests(void)
 {
 	VB2_DEBUG("Testing manual recovery action...\n");
@@ -831,6 +925,9 @@
 	menu_next_tests();
 	menu_select_tests();
 
+	/* Screen actions */
+	vb2_ui_developer_mode_boot_alternate_action_tests();
+
 	/* Global actions */
 	manual_recovery_action_tests();
 
diff --git a/tests/vb2_ui_tests.c b/tests/vb2_ui_tests.c
index 569a179..6691660 100644
--- a/tests/vb2_ui_tests.c
+++ b/tests/vb2_ui_tests.c
@@ -77,6 +77,7 @@
 
 static int mock_vbexlegacy_called;
 static enum VbAltFwIndex_t mock_altfw_num_last;
+static uint32_t mock_bootloader_count;
 
 static vb2_error_t mock_vbtlk_retval[32];
 static uint32_t mock_vbtlk_expected_flag[32];
@@ -302,6 +303,7 @@
 	/* For VbExLegacy */
 	mock_vbexlegacy_called = 0;
 	mock_altfw_num_last = -100;
+	mock_bootloader_count = 2;
 
 	/* For VbTryLoadKernel */
 	memset(mock_vbtlk_retval, 0, sizeof(mock_vbtlk_retval));
@@ -482,6 +484,11 @@
 	return VB2_SUCCESS;
 }
 
+uint32_t vb2ex_get_bootloader_count(void)
+{
+	return mock_bootloader_count;
+}
+
 vb2_error_t VbTryLoadKernel(struct vb2_context *c, uint32_t get_info_flags)
 {
 	int i = mock_iters;
@@ -669,6 +676,25 @@
 	TEST_TRUE(mock_get_timer_last - mock_time_start <
 		  30 * VB2_MSEC_PER_SEC, "  delay aborted");
 
+	/* Ctrl+L = boot legacy (allowed) */
+	reset_common_data(FOR_DEVELOPER);
+	mock_dev_boot_legacy_allowed = 1;
+	add_mock_keypress(VB_KEY_CTRL('L'));
+	TEST_EQ(vb2_developer_menu(ctx), VB2_REQUEST_SHUTDOWN,
+		"ctrl+l = boot legacy");
+	TEST_EQ(mock_vbexlegacy_called, 1, "  VbExLegacy called");
+	TEST_TRUE(mock_get_timer_last - mock_time_start <
+		  30 * VB2_MSEC_PER_SEC, "  delay aborted");
+
+	/* Ctrl+L = boot legacy (disallowed) */
+	reset_common_data(FOR_DEVELOPER);
+	add_mock_keypress(VB_KEY_CTRL('L'));
+	TEST_EQ(vb2_developer_menu(ctx), VB2_REQUEST_SHUTDOWN,
+		"ctrl+l = boot legacy");
+	TEST_EQ(mock_vbexlegacy_called, 0, "  VbExLegacy not called");
+	TEST_TRUE(mock_get_timer_last - mock_time_start <
+		  30 * VB2_MSEC_PER_SEC, "  delay aborted");
+
 	/* VB_BUTTON_VOL_UP_LONG_PRESS = boot external */
 	if (DETACHABLE) {
 		reset_common_data(FOR_DEVELOPER);
@@ -1164,6 +1190,7 @@
 
 	/* Dev mode: disabled item mask */
 	reset_common_data(FOR_DEVELOPER);
+	mock_dev_boot_legacy_allowed = 1;
 	add_mock_vbtlk(VB2_SUCCESS, VB_DISK_FLAG_FIXED);
 	TEST_EQ(vb2_developer_menu(ctx), VB2_SUCCESS,
 		"dev mode screen: no disabled item mask");
@@ -1171,6 +1198,7 @@
 		     MOCK_IGNORE, MOCK_IGNORE, 0x0, MOCK_IGNORE);
 
 	reset_common_data(FOR_DEVELOPER);
+	mock_dev_boot_legacy_allowed = 1;
 	gbb.flags |= VB2_GBB_FLAG_FORCE_DEV_SWITCH_ON;
 	add_mock_vbtlk(VB2_SUCCESS, VB_DISK_FLAG_FIXED);
 	TEST_EQ(vb2_developer_menu(ctx), VB2_SUCCESS,
@@ -1181,6 +1209,7 @@
 	reset_common_data(FOR_DEVELOPER);
 	add_mock_vbtlk(VB2_SUCCESS, VB_DISK_FLAG_FIXED);
 	mock_dev_boot_external_allowed = 0;
+	mock_dev_boot_legacy_allowed = 1;
 	TEST_EQ(vb2_developer_menu(ctx), VB2_SUCCESS,
 		"dev mode screen: disable boot external");
 	DISPLAYED_EQ("dev mode screen", VB2_SCREEN_DEVELOPER_MODE,
@@ -1237,8 +1266,19 @@
 	DISPLAYED_NO_EXTRA();
 
 	reset_common_data(FOR_DEVELOPER);  /* Select #2 by default */
+	mock_dev_boot_legacy_allowed = 1;
+	/* #4: Alternate boot */
+	add_mock_keypress(VB_KEY_DOWN);
+	add_mock_keypress(VB_KEY_DOWN);
+	add_mock_keypress(VB_KEY_ENTER);
+	add_mock_keypress(VB_KEY_ENTER);
+	TEST_EQ(vb2_developer_menu(ctx), VB2_REQUEST_SHUTDOWN,
+		"dev mode screen");
+	TEST_EQ(mock_vbexlegacy_called, 1, "  VbExLegacy called");
+
+	reset_common_data(FOR_DEVELOPER);  /* Select #2 by default */
 	add_mock_vbtlk(VB2_SUCCESS, VB_DISK_FLAG_FIXED);
-	/* #4: Advanced options */
+	/* #5: Advanced options */
 	add_mock_keypress(VB_KEY_DOWN);
 	add_mock_keypress(VB_KEY_DOWN);
 	add_mock_keypress(VB_KEY_ENTER);
@@ -1248,17 +1288,17 @@
 	add_mock_keypress(VB_KEY_DOWN);  /* Blocked */
 	TEST_EQ(vb2_developer_menu(ctx), VB2_REQUEST_SHUTDOWN,
 		"dev mode screen");
-	/* #4: Advanced options */
+	/* #5: Advanced options */
 	DISPLAYED_PASS();
 	DISPLAYED_PASS();
 	DISPLAYED_EQ("dev mode", VB2_SCREEN_DEVELOPER_MODE,
-		     MOCK_IGNORE, 4, MOCK_IGNORE, MOCK_IGNORE);
+		     MOCK_IGNORE, 5, MOCK_IGNORE, MOCK_IGNORE);
 	DISPLAYED_EQ("#4: advanced options", VB2_SCREEN_ADVANCED_OPTIONS,
 		     MOCK_IGNORE, MOCK_IGNORE, MOCK_IGNORE, MOCK_IGNORE);
 	/* End of menu */
 	DISPLAYED_PASS();
 	DISPLAYED_EQ("end of menu", VB2_SCREEN_DEVELOPER_MODE,
-		     MOCK_IGNORE, 5, MOCK_IGNORE, MOCK_IGNORE);
+		     MOCK_IGNORE, 6, MOCK_IGNORE, MOCK_IGNORE);
 	DISPLAYED_NO_EXTRA();
 
 	/* Advanced options screen */