blob: 2c42b8f43588927c06b9ed588f974f0116e572b2 [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "osboot.h"
#include <cmdline.h>
#include <device_id.h>
#include <fastboot.h>
#include <framebuffer.h>
#include <inet6.h>
#include <inttypes.h>
#include <limits.h>
#include <log.h>
#include <netifc.h>
#include <stdio.h>
#include <string.h>
#include <utf_conversion.h>
#include <xefi.h>
#include <zircon/boot/netboot.h>
#include <zircon/compiler.h>
#include <zircon/hw/gpt.h>
#include <efi/boot-services.h>
#include <efi/protocol/device-path.h>
#include <efi/protocol/graphics-output.h>
#include <efi/protocol/simple-text-input.h>
#include <efi/system-table.h>
#include "abr.h"
#include "avb.h"
#include "bootbyte.h"
#include "diskio.h"
#include "mdns.h"
#define DEFAULT_TIMEOUT 10
#define KBUFSIZE (32 * 1024 * 1024)
#define RBUFSIZE (512 * 1024 * 1024)
static nbfile nbzbi;
static nbfile nbcmdline;
static char cmdbuf[CMDLINE_MAX];
void print_cmdline(void) {
cmdline_to_string(cmdbuf, sizeof(cmdbuf));
LOG("cmdline: %s", cmdbuf);
}
nbfile* netboot_get_buffer(const char* name, size_t size) {
if (!strcmp(name, NB_KERNEL_FILENAME)) {
return &nbzbi;
}
if (!strcmp(name, NB_CMDLINE_FILENAME)) {
return &nbcmdline;
}
return NULL;
}
// Wait for a keypress from a set of valid keys. If 0 < timeout_s < INT_MAX, the
// first key in the set of valid keys will be returned after timeout_s seconds
// if no other valid key is pressed.
char key_prompt(const char* valid_keys, int timeout_s) {
if (strlen(valid_keys) < 1)
return 0;
if (timeout_s <= 0)
return valid_keys[0];
efi_status status;
efi_event timer_event = NULL;
if (timeout_s < INT_MAX) {
status = gBS->CreateEvent(EVT_TIMER, 0, NULL, NULL, &timer_event);
if (status != EFI_SUCCESS) {
ELOG_S(status, "could not create event timer");
return 0;
}
status = gBS->SetTimer(timer_event, TimerPeriodic, 10000000);
if (status != EFI_SUCCESS) {
ELOG_S(status, "could not set timer");
return 0;
}
}
bool cur_vis = gConOut->Mode->CursorVisible;
int32_t col = gConOut->Mode->CursorColumn;
int32_t row = gConOut->Mode->CursorRow;
gConOut->EnableCursor(gConOut, false);
char pressed = 0;
if (timeout_s < INT_MAX) {
LOG("Auto-boot in %ds", timeout_s);
}
do {
int key;
if (timeout_s == INT_MAX) {
key = xefi_getc(-1);
} else {
key = xefi_getc(0);
}
if (key > 0) {
char* which_key = strchr(valid_keys, key);
if (which_key) {
pressed = *which_key;
break;
}
}
if (timer_event != NULL && gBS->CheckEvent(timer_event) == EFI_SUCCESS) {
timeout_s--;
gConOut->SetCursorPosition(gConOut, col, row);
LOG("Auto-boot in %ds", timeout_s);
}
} while (timeout_s);
if (timer_event != NULL) {
gBS->CloseEvent(timer_event);
}
gConOut->EnableCursor(gConOut, cur_vis);
if (timeout_s > 0 && pressed) {
return pressed;
}
// Default to first key in list
return valid_keys[0];
}
void list_abr_info(void) {
for (uint32_t i = 0; i <= kAbrSlotIndexR; i++) {
AbrSlotInfo info;
AbrResult result;
result = zircon_abr_get_slot_info(i, &info);
if (result != kAbrResultOk) {
ELOG("Failed to get zircon%s slot info: %d", AbrGetSlotSuffix(i), result);
return;
}
LOG("Slot zircon%s : Bootable? %d, Successful boot? %d, Active? %d, Retry# %d",
AbrGetSlotSuffix(i), info.is_bootable, info.is_marked_successful, info.is_active,
info.num_tries_remaining);
}
}
void do_select_fb(void) {
uint32_t cur_mode = get_gfx_mode();
uint32_t max_mode = get_gfx_max_mode();
while (true) {
printf("\n");
print_fb_modes();
printf("Choose a framebuffer mode or press (b) to return to the menu\n");
char key = key_prompt("b0123456789", INT_MAX);
if (key == 'b')
break;
if ((uint32_t)(key - '0') >= max_mode) {
printf("invalid mode: %c\n", key);
continue;
}
set_gfx_mode(key - '0');
printf("Use \"bootloader.fbres=%ux%u\" to use this resolution by default\n", get_gfx_hres(),
get_gfx_vres());
printf("Press space to accept or (r) to choose again ...");
key = key_prompt("r ", 5);
if (key == ' ') {
return;
}
set_gfx_mode(cur_mode);
}
}
void do_fastboot(efi_handle img, efi_system_table* sys, uint32_t namegen) {
LOG("entering fastboot mode");
fb_bootimg_t bootimg;
mdns_start(namegen);
fb_poll_next_action action = POLL;
while (action == POLL) {
mdns_poll();
action = fb_poll(&bootimg);
}
switch (action) {
case BOOT_FROM_RAM:
mdns_stop();
zbi_boot(img, sys, bootimg.kernel_start, bootimg.kernel_size);
break;
case CONTINUE_BOOT:
case POLL:
break;
case REBOOT: {
efi_status status = gSys->RuntimeServices->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL);
if (status != EFI_SUCCESS) {
ELOG_S(status, "Failed to reboot");
}
}
}
mdns_stop();
}
void do_bootmenu(bool have_fb) {
const char* menukeys;
if (have_fb) {
menukeys = "rfax";
} else {
menukeys = "rax";
}
while (true) {
printf(" BOOT MENU \n");
printf(" --------- \n");
if (have_fb)
printf(" (f) list framebuffer modes\n");
printf(" (a) List abr info\n");
printf(" (r) reset\n");
printf(" (x) exit menu\n");
printf("\n");
char key = key_prompt(menukeys, INT_MAX);
switch (key) {
case 'f': {
do_select_fb();
break;
}
case 'a':
list_abr_info();
break;
case 'r':
gSys->RuntimeServices->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL);
break;
case 'x':
default:
return;
}
}
}
static char netboot_cmdline[CMDLINE_MAX];
void do_netboot(void) {
efi_physical_addr mem = 0xFFFFFFFF;
if (gBS->AllocatePages(AllocateMaxAddress, EfiLoaderData, KBUFSIZE / 4096, &mem)) {
ELOG("Failed to allocate network io buffer");
return;
}
nbzbi.data = (void*)mem;
nbzbi.size = KBUFSIZE;
nbcmdline.data = (void*)netboot_cmdline;
nbcmdline.size = sizeof(netboot_cmdline);
nbcmdline.offset = 0;
LOG("NetBoot server started");
efi_tpl prev_tpl = gBS->RaiseTPL(TPL_NOTIFY);
while (true) {
int n = netboot_poll();
if (n < 1) {
continue;
}
if (nbzbi.offset < 32768) {
// too small to be a kernel
continue;
}
uint8_t* x = nbzbi.data;
if ((x[0] == 'M') && (x[1] == 'Z') && (x[0x80] == 'P') && (x[0x81] == 'E')) {
size_t exitdatasize;
efi_status r;
efi_handle h;
efi_device_path_hw_memmap mempath[2] = {
{
.Header =
{
.Type = DEVICE_PATH_HARDWARE,
.SubType = DEVICE_PATH_HW_MEMMAP,
.Length =
{
(uint8_t)(sizeof(efi_device_path_hw_memmap) & 0xff),
(uint8_t)((sizeof(efi_device_path_hw_memmap) >> 8) & 0xff),
},
},
.MemoryType = EfiLoaderData,
.StartAddress = (efi_physical_addr)nbzbi.data,
.EndAddress = (efi_physical_addr)(nbzbi.data + nbzbi.offset),
},
{
.Header =
{
.Type = DEVICE_PATH_END,
.SubType = DEVICE_PATH_ENTIRE_END,
.Length =
{
(uint8_t)(sizeof(efi_device_path_protocol) & 0xff),
(uint8_t)((sizeof(efi_device_path_protocol) >> 8) & 0xff),
},
},
},
};
LOG("Attempting to run EFI binary");
r = gBS->LoadImage(false, gImg, (efi_device_path_protocol*)mempath, (void*)nbzbi.data,
nbzbi.offset, &h);
if (EFI_ERROR(r)) {
ELOG_S(r, "LoadImage failed");
continue;
}
r = gBS->StartImage(h, &exitdatasize, NULL);
if (EFI_ERROR(r)) {
ELOG_S(r, "StartImage failed");
continue;
}
LOG("NetBoot server resuming");
continue;
}
// make sure network traffic is not in flight, etc
netboot_close();
// Restore the TPL before booting the kernel, or failing to netboot
gBS->RestoreTPL(prev_tpl);
cmdline_append((void*)nbcmdline.data, nbcmdline.offset);
print_cmdline();
const char* fbres = cmdline_get("bootloader.fbres", NULL);
if (fbres) {
set_gfx_mode_from_cmdline(fbres);
}
zbi_boot(gImg, gSys, (void*)nbzbi.data, nbzbi.offset);
break;
}
}
// Runs the top-level boot menu.
//
// Args:
// have_network: true if we have a working network interface.
// have_fb: true if we have a framebuffer.
//
// Returns the user's selection.
static BootAction main_boot_menu(bool have_network, bool have_fb) {
int timeout_s = cmdline_get_uint32("bootloader.timeout", DEFAULT_TIMEOUT);
while (1) {
#define VALID_KEYS_COMMON "\r\nbf1m2rz"
const char* valid_keys = VALID_KEYS_COMMON;
printf(
"\n"
"Boot options:\n"
" <enter> to continue default boot\n"
" b) boot menu\n"
" f) fastboot\n"
" 1) set A slot active and boot (alternate: m)\n"
" 2) set B slot active and boot\n"
" r) one-time boot R slot (alternate: z)\n");
if (have_network) {
valid_keys = VALID_KEYS_COMMON "n";
printf(" n) network boot\n");
}
#undef VALID_KEYS_COMMON
char key = key_prompt(valid_keys, timeout_s);
printf("\n\n");
switch (key) {
case '\r':
case '\n':
// <enter> or timeout, use the default boot behavior.
return kBootActionDefault;
case 'b':
// Run the sub-menu then repeat this top-level menu.
do_bootmenu(have_fb);
break;
case 'n':
return kBootActionNetboot;
case 'f':
return kBootActionFastboot;
case '1':
case 'm':
return kBootActionSlotA;
case '2':
return kBootActionSlotB;
case 'r':
case 'z':
return kBootActionSlotR;
}
}
}
BootAction get_boot_action(bool have_network, bool have_fb) {
// 1. Bootbyte.
// Ignore the reboot count, we have a more robust mechanism now in the A/B/R
// retry count.
unsigned char bootbyte = bootbyte_read() & ~RTC_BOOT_COUNT_MASK;
bootbyte_clear();
if (bootbyte == RTC_BOOT_RECOVERY) {
return kBootActionSlotR;
} else if (bootbyte == RTC_BOOT_BOOTLOADER) {
return kBootActionFastboot;
}
// 2. Boot menu.
BootAction boot_action = main_boot_menu(have_network, have_fb);
// 3. Commandline, options are "local", "zedboot", or "network".
if (boot_action == kBootActionDefault) {
// If no commandline, default to network (TODO: Is this still needed?).
const char* defboot = cmdline_get("bootloader.default", "network");
if (strcmp(defboot, "local") == 0) {
return kBootActionDefault;
} else if (strcmp(defboot, "zedboot") == 0) {
return kBootActionSlotR;
} else if (strcmp(defboot, "network") == 0) {
if (have_network) {
return kBootActionNetboot;
} else {
LOG("No network, skipping netboot and booting from disk");
return kBootActionDefault;
}
} else {
WLOG("Ignoring unknown bootloader.default: '%s'", defboot);
}
}
return boot_action;
}
size_t kernel_zone_size;
efi_physical_addr kernel_zone_base;
efi_status efi_main(efi_handle img, efi_system_table* sys) {
xefi_init(img, sys);
gConOut->ClearScreen(gConOut);
uint64_t mmio;
if (xefi_find_pci_mmio(gBS, 0x0C, 0x03, 0x30, &mmio) == EFI_SUCCESS) {
char tmp[32];
sprintf(tmp, "%#" PRIx64, mmio);
cmdline_set("xdc.mmio", tmp);
}
// Prepend any EFI app command line arguments
cmdline_append_load_options();
// Load the cmdline
size_t csz = 0;
char* cmdline_file = xefi_load_file(L"cmdline", &csz, 0);
if (cmdline_file) {
cmdline_append(cmdline_file, csz);
}
uint32_t enable_serial = cmdline_get_uint32("bootloader.serial", 0);
if (!enable_serial) {
// TODO(https://fxbug.dev/72512): Remove when GCE handles serial i/o.
gSerial = NULL;
}
efi_graphics_output_protocol* gop;
efi_status status = gBS->LocateProtocol(&GraphicsOutputProtocol, NULL, (void**)&gop);
bool have_fb = !EFI_ERROR(status);
if (have_fb) {
const char* fbres = cmdline_get("bootloader.fbres", NULL);
if (fbres) {
set_gfx_mode_from_cmdline(fbres);
}
draw_logo();
}
int32_t prev_attr = gConOut->Mode->Attribute;
gConOut->SetAttribute(gConOut, EFI_LIGHTZIRCON | EFI_BACKGROUND_BLACK);
draw_version(BOOTLOADER_VERSION);
gConOut->SetAttribute(gConOut, prev_attr);
if (have_fb) {
LOG("Framebuffer base is at %" PRIx64, gop->Mode->FrameBufferBase);
}
// Set aside space for the kernel down at the 1MB mark up front
// to avoid other allocations getting in the way.
// The kernel itself is about 1MB, but we leave generous space
// for its BSS afterwards.
//
// Previously we requested 32MB but that caused issues. When the kernel
// becomes relocatable this won't be an problem. See fxbug.dev/32223.
kernel_zone_base = 0x100000;
kernel_zone_size = 8 * 1024 * 1024;
if (gBS->AllocatePages(AllocateAddress, EfiLoaderData, BYTES_TO_PAGES(kernel_zone_size),
&kernel_zone_base)) {
ELOG("boot: cannot obtain %zu bytes for kernel @ %p", kernel_zone_size,
(void*)kernel_zone_base);
kernel_zone_size = 0;
}
// HACK: Try again with a smaller size - certain platforms (ex: GCE) are unable
// to support a large fixed allocation at 0x100000.
if (kernel_zone_size == 0) {
kernel_zone_size = 3 * 1024 * 1024;
efi_status status = gBS->AllocatePages(AllocateAddress, EfiLoaderData,
BYTES_TO_PAGES(kernel_zone_size), &kernel_zone_base);
if (status) {
ELOG("boot: cannot obtain %zu bytes for kernel @ %p", kernel_zone_size,
(void*)kernel_zone_base);
kernel_zone_size = 0;
}
}
LOG("Kernel allocation done");
const char* nodename = cmdline_get("zircon.nodename", "");
uint32_t namegen = cmdline_get_uint32("zircon.namegen", 1);
// See if there's a network interface
bool have_network = netboot_init(nodename, namegen) == 0;
if (have_network) {
if (have_fb) {
draw_nodename(netboot_nodename());
} else {
LOG("Nodename: %s", netboot_nodename());
}
// If nodename was set through cmdline earlier in the code path then
// netboot_nodename will return that same value, otherwise it will
// return the generated value in which case it needs to be added to
// the command line arguments.
if (nodename[0] == 0) {
cmdline_set("zircon.nodename", netboot_nodename());
}
}
printf("\n\n");
print_cmdline();
// TODO(jonmayo): loading these images before making a decision is very wasteful.
size_t zedboot_size = 0;
void* zedboot_kernel = NULL;
size_t ksz = 0;
void* kernel = NULL;
size_t ksz_b = 0;
void* kernel_b = NULL;
struct {
const char16_t* wfilename;
const char* filename;
uint8_t guid_value[GPT_GUID_LEN];
const char* guid_name;
void** kernel;
size_t* size;
} boot_list[] = {
// ZIRCON-A with legacy fallback filename on EFI partition
{L"zircon.bin", "zircon.bin", GUID_ZIRCON_A_VALUE, GUID_ZIRCON_A_NAME, &kernel, &ksz},
// no filename fallback for ZIRCON-B
{NULL, NULL, GUID_ZIRCON_B_VALUE, GUID_ZIRCON_B_NAME, &kernel_b, &ksz_b},
// Recovery / ZIRCON-R
{L"zedboot.bin", "zedboot.bin", GUID_ZIRCON_R_VALUE, GUID_ZIRCON_R_NAME, &zedboot_kernel,
&zedboot_size},
};
unsigned i;
// Check for command-line overrides for files
const char* zircon_a_filename = cmdline_get("bootloader.zircon-a", NULL);
if (zircon_a_filename != NULL) {
static uint16_t zircon_a_wfilename[128];
size_t wfilename_converted_size = sizeof(zircon_a_wfilename);
if (utf8_to_utf16((const uint8_t*)zircon_a_filename, strlen(zircon_a_filename) + 1,
zircon_a_wfilename, &wfilename_converted_size) == ZX_OK) {
if (wfilename_converted_size >= sizeof(zircon_a_wfilename)) {
WLOG("bootloader.zircon-a string truncated");
wfilename_converted_size = sizeof(zircon_a_wfilename) - sizeof(uint16_t);
}
zircon_a_wfilename[wfilename_converted_size / sizeof(uint16_t)] = 0;
boot_list[0].wfilename = zircon_a_wfilename;
boot_list[0].filename = zircon_a_filename;
LOG("Using zircon-a=%s", zircon_a_filename);
}
}
const char* zircon_b_filename = cmdline_get("bootloader.zircon-b", NULL);
if (zircon_b_filename != NULL) {
static uint16_t zircon_b_wfilename[128];
size_t wfilename_converted_size = sizeof(zircon_b_wfilename);
if (utf8_to_utf16((const uint8_t*)zircon_b_filename, strlen(zircon_b_filename) + 1,
zircon_b_wfilename, &wfilename_converted_size) == ZX_OK) {
if (wfilename_converted_size >= sizeof(zircon_b_wfilename)) {
WLOG("bootloader.zircon-b string truncated");
wfilename_converted_size = sizeof(zircon_b_wfilename) - sizeof(uint16_t);
}
zircon_b_wfilename[wfilename_converted_size / sizeof(uint16_t)] = 0;
boot_list[1].wfilename = zircon_b_wfilename;
boot_list[1].filename = zircon_b_filename;
LOG("Using zircon-b=%s", zircon_b_filename);
}
}
const char* zircon_r_filename = cmdline_get("bootloader.zircon-r", NULL);
if (zircon_r_filename != NULL) {
static uint16_t zircon_r_wfilename[128];
size_t wfilename_converted_size = sizeof(zircon_r_wfilename);
if (utf8_to_utf16((const uint8_t*)zircon_r_filename, strlen(zircon_r_filename) + 1,
zircon_r_wfilename, &wfilename_converted_size) == ZX_OK) {
if (wfilename_converted_size >= sizeof(zircon_r_wfilename)) {
WLOG("bootloader.zircon-r string truncated");
wfilename_converted_size = sizeof(zircon_r_wfilename) - sizeof(uint16_t);
}
zircon_r_wfilename[wfilename_converted_size / sizeof(uint16_t)] = 0;
boot_list[2].wfilename = zircon_r_wfilename;
boot_list[2].filename = zircon_r_filename;
LOG("Using zircon-r=%s", zircon_r_filename);
}
}
// Look for ZIRCON-A/B/R partitions
for (i = 0; i < sizeof(boot_list) / sizeof(*boot_list); i++) {
*boot_list[i].kernel = image_load_from_disk(img, sys, EXTRA_ZBI_ITEM_SPACE, boot_list[i].size,
boot_list[i].guid_value, boot_list[i].guid_name);
if (*boot_list[i].kernel != NULL) {
LOG("zircon image loaded from zircon partition %s", boot_list[i].guid_name);
} else if (boot_list[i].wfilename != NULL) {
*boot_list[i].kernel = xefi_load_file(boot_list[i].wfilename, boot_list[i].size, 0);
if (image_is_valid(*boot_list[i].kernel, *boot_list[i].size)) {
LOG("%s is a valid image", boot_list[i].filename);
} else {
*boot_list[i].kernel = NULL;
*boot_list[i].size = 0;
LOG("%s is not a valid image", boot_list[i].filename);
}
}
}
if (!have_network && zedboot_kernel == NULL && kernel == NULL && kernel_b == NULL) {
ELOG("No valid kernel image found to load. Abort.");
xefi_getc(-1);
return EFI_SUCCESS;
}
// Disable WDT
// The second parameter can be any value outside of the range [0,0xffff]
gBS->SetWatchdogTimer(0, 0x10000, 0, NULL);
bool force_recovery = false;
BootAction boot_action = get_boot_action(have_network, have_fb);
switch (boot_action) {
case kBootActionDefault:
break;
case kBootActionFastboot:
// do_fastboot() only returns on `fastboot continue`, in which case we
// continue to boot from disk.
do_fastboot(img, sys, namegen);
break;
case kBootActionNetboot:
// do_netboot() only returns on error.
do_netboot();
ELOG("netboot failure");
xefi_getc(-1);
return EFI_SUCCESS;
case kBootActionSlotA:
zircon_abr_set_slot_active(kAbrSlotIndexA);
break;
case kBootActionSlotB:
zircon_abr_set_slot_active(kAbrSlotIndexB);
break;
case kBootActionSlotR:
// We could use zircon_abr_set_oneshot_recovery() here but there's no
// need to write to disk when we can just track it locally.
force_recovery = true;
break;
}
// If we got here, boot from disk according to A/B/R metadata.
// Consider switching over to using the zircon_boot library which has a lot
// of this logic built-in.
void* zbi = NULL;
size_t zbi_size = 0;
const char* slot_string = NULL;
AbrSlotIndex slot;
while (1) {
slot = force_recovery ? kAbrSlotIndexR : zircon_abr_get_boot_slot(true);
switch (slot) {
case kAbrSlotIndexA:
zbi = kernel;
zbi_size = ksz;
slot_string = "-a";
break;
case kAbrSlotIndexB:
zbi = kernel_b;
zbi_size = ksz_b;
slot_string = "-b";
break;
case kAbrSlotIndexR:
zbi = zedboot_kernel;
zbi_size = zedboot_size;
slot_string = "-r";
break;
}
// No verified boot yet, if we have a non-NULL ZBI we assume it's good.
if (zbi != NULL) {
LOG("Booting slot %d", slot);
break;
}
LOG("Failed to find a kernel in slot %d", slot);
// R is always the last slot to try, if we got here there's nothing else
// we can do.
if (slot == kAbrSlotIndexR) {
ELOG("no valid kernel was found");
break;
}
// Move to the next slot since we don't have a kernel in this one.
AbrResult result = zircon_abr_mark_slot_unbootable(slot);
if (result != kAbrResultOk) {
ELOG("failed to mark slot %d unbootable (%d)", slot, result);
break;
}
}
if (zbi != NULL) {
// Only set these flags when not booting zedboot. See http://fxbug.dev/72713
// for more information.
if (slot != kAbrSlotIndexR && is_booting_from_usb(img, sys)) {
LOG("booting from usb");
// TODO(fxbug.dev/44586): remove devmgr.bind-eager once better driver
// prioritisation exists.
static const char* usb_boot_args = "boot.usb=true devmgr.bind-eager=usb_composite";
cmdline_append(usb_boot_args, strlen(usb_boot_args));
}
zircon_abr_update_boot_slot_metadata();
append_avb_zbi_items(img, sys, zbi, zbi_size, slot_string);
zbi_boot(img, sys, zbi, zbi_size);
}
// We only get here if we ran out of slots to try or zbi_boot() failed.
ELOG("failed to boot from disk");
xefi_getc(-1);
return EFI_SUCCESS;
}