mirror of
https://git.proxmox.com/git/efi-boot-shim
synced 2025-04-30 10:41:29 +00:00
430 lines
10 KiB
C
430 lines
10 KiB
C
// SPDX-License-Identifier: BSD-2-Clause-Patent
|
|
|
|
/*
|
|
* netboot - trivial UEFI first-stage bootloader netboot support
|
|
*
|
|
* Copyright Red Hat, Inc
|
|
* Author: Matthew Garrett
|
|
*
|
|
* Significant portions of this code are derived from Tianocore
|
|
* (http://tianocore.sf.net) and are Copyright 2009-2012 Intel
|
|
* Corporation.
|
|
*/
|
|
|
|
#include "shim.h"
|
|
|
|
#define ntohs(x) __builtin_bswap16(x) /* supported both by GCC and clang */
|
|
#define htons(x) ntohs(x)
|
|
|
|
/* TFTP error codes from RFC 1350 */
|
|
#define TFTP_ERROR_NOT_DEFINED 0 /* Not defined, see error message (if any). */
|
|
#define TFTP_ERROR_NOT_FOUND 1 /* File not found. */
|
|
#define TFTP_ERROR_ACCESS 2 /* Access violation. */
|
|
#define TFTP_ERROR_NO_SPACE 3 /* Disk full or allocation exceeded. */
|
|
#define TFTP_ERROR_ILLEGAL_OP 4 /* Illegal TFTP operation. */
|
|
#define TFTP_ERROR_UNKNOWN_ID 5 /* Unknown transfer ID. */
|
|
#define TFTP_ERROR_EXISTS 6 /* File already exists. */
|
|
#define TFTP_ERROR_NO_USER 7 /* No such user. */
|
|
|
|
static EFI_PXE_BASE_CODE *pxe;
|
|
static EFI_IP_ADDRESS tftp_addr;
|
|
static CHAR8 *full_path;
|
|
|
|
|
|
typedef struct {
|
|
UINT16 OpCode;
|
|
UINT16 Length;
|
|
UINT8 Data[1];
|
|
} EFI_DHCP6_PACKET_OPTION;
|
|
|
|
/*
|
|
* usingNetboot
|
|
* Returns TRUE if we identify a protocol that is enabled and Providing us with
|
|
* the needed information to fetch a grubx64.efi image
|
|
*/
|
|
BOOLEAN findNetboot(EFI_HANDLE device)
|
|
{
|
|
EFI_STATUS efi_status;
|
|
|
|
efi_status = BS->HandleProtocol(device, &PxeBaseCodeProtocol,
|
|
(VOID **) &pxe);
|
|
if (EFI_ERROR(efi_status)) {
|
|
pxe = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
if (!pxe || !pxe->Mode) {
|
|
pxe = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
if (!pxe->Mode->Started || !pxe->Mode->DhcpAckReceived) {
|
|
pxe = NULL;
|
|
return FALSE;
|
|
}
|
|
|
|
/*
|
|
* We've located a pxe protocol handle thats been started and has
|
|
* received an ACK, meaning its something we'll be able to get
|
|
* tftp server info out of
|
|
*/
|
|
return TRUE;
|
|
}
|
|
|
|
static CHAR8 *get_v6_bootfile_url(EFI_PXE_BASE_CODE_DHCPV6_PACKET *pkt)
|
|
{
|
|
void *optr = NULL, *end = NULL;
|
|
EFI_DHCP6_PACKET_OPTION *option = NULL;
|
|
CHAR8 *url = NULL;
|
|
UINT32 urllen = 0;
|
|
|
|
optr = pkt->DhcpOptions;
|
|
end = optr + sizeof(pkt->DhcpOptions);
|
|
|
|
for (;;) {
|
|
option = (EFI_DHCP6_PACKET_OPTION *)optr;
|
|
|
|
if (ntohs(option->OpCode) == 0)
|
|
break;
|
|
|
|
if (ntohs(option->OpCode) == 59) {
|
|
/* This is the bootfile url option */
|
|
urllen = ntohs(option->Length);
|
|
if ((void *)(option->Data + urllen) > end)
|
|
break;
|
|
url = AllocateZeroPool(urllen + 1);
|
|
if (!url)
|
|
break;
|
|
memcpy(url, option->Data, urllen);
|
|
return url;
|
|
}
|
|
optr += 4 + ntohs(option->Length);
|
|
if (optr + sizeof(EFI_DHCP6_PACKET_OPTION) > end)
|
|
break;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static CHAR16 str2ns(CHAR8 *str)
|
|
{
|
|
CHAR16 ret = 0;
|
|
CHAR8 v;
|
|
for(;*str;str++) {
|
|
if ('0' <= *str && *str <= '9')
|
|
v = *str - '0';
|
|
else if ('A' <= *str && *str <= 'F')
|
|
v = *str - 'A' + 10;
|
|
else if ('a' <= *str && *str <= 'f')
|
|
v = *str - 'a' + 10;
|
|
else
|
|
v = 0;
|
|
ret = (ret << 4) + v;
|
|
}
|
|
return htons(ret);
|
|
}
|
|
|
|
static CHAR8 *str2ip6(CHAR8 *str)
|
|
{
|
|
UINT8 i = 0, j = 0, p = 0;
|
|
size_t len = 0, dotcount = 0;
|
|
enum { MAX_IP6_DOTS = 7 };
|
|
CHAR8 *a = NULL, *b = NULL, t = 0;
|
|
static UINT16 ip[8];
|
|
|
|
memset(ip, 0, sizeof(ip));
|
|
|
|
/* Count amount of ':' to prevent overflows.
|
|
* max. count = 7. Returns an invalid ip6 that
|
|
* can be checked against
|
|
*/
|
|
for (a = str; *a != 0; ++a) {
|
|
if (*a == ':')
|
|
++dotcount;
|
|
}
|
|
if (dotcount > MAX_IP6_DOTS)
|
|
return (CHAR8 *)ip;
|
|
|
|
len = strlen(str);
|
|
a = b = str;
|
|
for (i = p = 0; i < len; i++, b++) {
|
|
if (*b != ':')
|
|
continue;
|
|
*b = '\0';
|
|
ip[p++] = str2ns(a);
|
|
*b = ':';
|
|
a = b + 1;
|
|
if (b[1] == ':' )
|
|
break;
|
|
}
|
|
a = b = (str + len);
|
|
for (j = len, p = 7; j > i; j--, a--) {
|
|
if (*a != ':')
|
|
continue;
|
|
t = *b;
|
|
*b = '\0';
|
|
ip[p--] = str2ns(a+1);
|
|
*b = t;
|
|
b = a;
|
|
}
|
|
return (CHAR8 *)ip;
|
|
}
|
|
|
|
static BOOLEAN extract_tftp_info(CHAR8 *url, CHAR8 *name)
|
|
{
|
|
CHAR8 *start, *end;
|
|
CHAR8 ip6str[40];
|
|
CHAR8 ip6inv[16];
|
|
int template_len = 0;
|
|
CHAR8 *template;
|
|
|
|
while (name[template_len++] != '\0');
|
|
template = (CHAR8 *)AllocatePool((template_len + 1) * sizeof (CHAR8));
|
|
translate_slashes(template, name);
|
|
|
|
// to check against str2ip6() errors
|
|
memset(ip6inv, 0, sizeof(ip6inv));
|
|
|
|
if (strncmp((const char *)url, (const char *)"tftp://", 7)) {
|
|
console_print(L"URLS MUST START WITH tftp://\n");
|
|
FreePool(template);
|
|
return FALSE;
|
|
}
|
|
start = url + 7;
|
|
if (*start != '[') {
|
|
console_print(L"TFTP SERVER MUST BE ENCLOSED IN [..]\n");
|
|
FreePool(template);
|
|
return FALSE;
|
|
}
|
|
|
|
start++;
|
|
end = start;
|
|
while ((*end != '\0') && (*end != ']')) {
|
|
end++;
|
|
if (end - start >= (int)sizeof(ip6str)) {
|
|
console_print(L"TFTP URL includes malformed IPv6 address\n");
|
|
FreePool(template);
|
|
return FALSE;
|
|
}
|
|
}
|
|
if (*end == '\0') {
|
|
console_print(L"TFTP SERVER MUST BE ENCLOSED IN [..]\n");
|
|
FreePool(template);
|
|
return FALSE;
|
|
}
|
|
memset(ip6str, 0, sizeof(ip6str));
|
|
memcpy(ip6str, start, end - start);
|
|
end++;
|
|
memcpy(&tftp_addr.v6, str2ip6(ip6str), 16);
|
|
if (memcmp(&tftp_addr.v6, ip6inv, sizeof(ip6inv)) == 0) {
|
|
FreePool(template);
|
|
return FALSE;
|
|
}
|
|
full_path = AllocateZeroPool(strlen(end)+strlen(template)+1);
|
|
if (!full_path) {
|
|
FreePool(template);
|
|
return FALSE;
|
|
}
|
|
memcpy(full_path, end, strlen(end));
|
|
end = (CHAR8 *)strrchr((char *)full_path, '/');
|
|
if (!end)
|
|
end = (CHAR8 *)full_path;
|
|
memcpy(end, template, strlen(template));
|
|
end[strlen(template)] = '\0';
|
|
|
|
FreePool(template);
|
|
return TRUE;
|
|
}
|
|
|
|
static EFI_STATUS parseDhcp6(CHAR8 *name)
|
|
{
|
|
EFI_PXE_BASE_CODE_DHCPV6_PACKET *packet = (EFI_PXE_BASE_CODE_DHCPV6_PACKET *)&pxe->Mode->DhcpAck.Raw;
|
|
CHAR8 *bootfile_url;
|
|
|
|
bootfile_url = get_v6_bootfile_url(packet);
|
|
if (!bootfile_url)
|
|
return EFI_NOT_FOUND;
|
|
if (extract_tftp_info(bootfile_url, name) == FALSE) {
|
|
FreePool(bootfile_url);
|
|
return EFI_NOT_FOUND;
|
|
}
|
|
FreePool(bootfile_url);
|
|
return EFI_SUCCESS;
|
|
}
|
|
|
|
static EFI_STATUS parseDhcp4(CHAR8 *name)
|
|
{
|
|
CHAR8 *template;
|
|
INTN template_len = 0;
|
|
UINTN template_ofs = 0;
|
|
EFI_PXE_BASE_CODE_DHCPV4_PACKET* pkt_v4 = (EFI_PXE_BASE_CODE_DHCPV4_PACKET *)&pxe->Mode->DhcpAck.Dhcpv4;
|
|
|
|
while (name[template_len++] != '\0');
|
|
template = (CHAR8 *)AllocatePool((template_len + 1) * sizeof (CHAR8));
|
|
translate_slashes(template, name);
|
|
template_len = strlen(template) + 1;
|
|
|
|
if(pxe->Mode->ProxyOfferReceived) {
|
|
/*
|
|
* Proxy should not have precedence. Check if DhcpAck
|
|
* contained boot info.
|
|
*/
|
|
if(pxe->Mode->DhcpAck.Dhcpv4.BootpBootFile[0] == '\0')
|
|
pkt_v4 = &pxe->Mode->ProxyOffer.Dhcpv4;
|
|
}
|
|
|
|
if(pxe->Mode->PxeReplyReceived) {
|
|
/*
|
|
* If we have no bootinfo yet search for it in the PxeReply.
|
|
* Some mainboards run into this when the server uses boot menus
|
|
*/
|
|
if(pkt_v4->BootpBootFile[0] == '\0' && pxe->Mode->PxeReply.Dhcpv4.BootpBootFile[0] != '\0')
|
|
pkt_v4 = &pxe->Mode->PxeReply.Dhcpv4;
|
|
}
|
|
|
|
INTN dir_len = strnlen((CHAR8 *)pkt_v4->BootpBootFile, 127);
|
|
INTN i;
|
|
UINT8 *dir = pkt_v4->BootpBootFile;
|
|
|
|
for (i = dir_len; i >= 0; i--) {
|
|
if ((dir[i] == '/') || (dir[i] == '\\'))
|
|
break;
|
|
}
|
|
dir_len = (i >= 0) ? i + 1 : 0;
|
|
|
|
full_path = AllocateZeroPool(dir_len + template_len);
|
|
|
|
if (!full_path) {
|
|
FreePool(template);
|
|
return EFI_OUT_OF_RESOURCES;
|
|
}
|
|
|
|
if (dir_len > 0) {
|
|
strncpy(full_path, (CHAR8 *)dir, dir_len);
|
|
if (full_path[dir_len-1] == '/' && template[0] == '/')
|
|
full_path[dir_len-1] = '\0';
|
|
/*
|
|
* If the path from DHCP is using backslash instead of slash,
|
|
* accept that and use it in the template in the same position
|
|
* as well.
|
|
*/
|
|
if (full_path[dir_len-1] == '\\' && template[0] == '/') {
|
|
full_path[dir_len-1] = '\0';
|
|
template[0] = '\\';
|
|
}
|
|
}
|
|
if (dir_len == 0 && dir[0] != '/' && template[0] == '/')
|
|
template_ofs++;
|
|
strcat(full_path, template + template_ofs);
|
|
memcpy(&tftp_addr.v4, pkt_v4->BootpSiAddr, 4);
|
|
|
|
FreePool(template);
|
|
return EFI_SUCCESS;
|
|
}
|
|
|
|
EFI_STATUS parseNetbootinfo(EFI_HANDLE image_handle UNUSED, CHAR8 *netbootname)
|
|
{
|
|
|
|
EFI_STATUS efi_status;
|
|
|
|
if (!pxe)
|
|
return EFI_NOT_READY;
|
|
|
|
memset((UINT8 *)&tftp_addr, 0, sizeof(tftp_addr));
|
|
|
|
/*
|
|
* If we've discovered an active pxe protocol figure out
|
|
* if its ipv4 or ipv6
|
|
*/
|
|
if (pxe->Mode->UsingIpv6){
|
|
efi_status = parseDhcp6(netbootname);
|
|
} else
|
|
efi_status = parseDhcp4(netbootname);
|
|
return efi_status;
|
|
}
|
|
|
|
/* Convert a TFTP error code to an EFI status code. */
|
|
static EFI_STATUS
|
|
status_from_error(UINT8 error_code)
|
|
{
|
|
switch (error_code) {
|
|
case TFTP_ERROR_NOT_FOUND:
|
|
return EFI_NOT_FOUND;
|
|
case TFTP_ERROR_ACCESS:
|
|
case TFTP_ERROR_NO_USER:
|
|
return EFI_ACCESS_DENIED;
|
|
case TFTP_ERROR_NO_SPACE:
|
|
return EFI_VOLUME_FULL;
|
|
case TFTP_ERROR_ILLEGAL_OP:
|
|
return EFI_PROTOCOL_ERROR;
|
|
case TFTP_ERROR_UNKNOWN_ID:
|
|
return EFI_INVALID_PARAMETER;
|
|
case TFTP_ERROR_EXISTS:
|
|
return EFI_WRITE_PROTECTED;
|
|
case TFTP_ERROR_NOT_DEFINED:
|
|
default:
|
|
/* Use a generic TFTP error for anything else. */
|
|
return EFI_TFTP_ERROR;
|
|
}
|
|
}
|
|
|
|
EFI_STATUS FetchNetbootimage(EFI_HANDLE image_handle UNUSED, VOID **buffer,
|
|
UINT64 *bufsiz, int flags)
|
|
{
|
|
EFI_STATUS efi_status;
|
|
EFI_PXE_BASE_CODE_TFTP_OPCODE read = EFI_PXE_BASE_CODE_TFTP_READ_FILE;
|
|
BOOLEAN overwrite = FALSE;
|
|
BOOLEAN nobuffer = FALSE;
|
|
UINTN blksz = 512;
|
|
|
|
if (~flags & SUPPRESS_NETBOOT_OPEN_FAILURE_NOISE)
|
|
console_print(L"Fetching Netboot Image %a\n", full_path);
|
|
if (*buffer == NULL) {
|
|
*buffer = AllocatePool(4096 * 1024);
|
|
if (!*buffer)
|
|
return EFI_OUT_OF_RESOURCES;
|
|
*bufsiz = 4096 * 1024;
|
|
}
|
|
|
|
try_again:
|
|
efi_status = pxe->Mtftp(pxe, read, *buffer, overwrite, bufsiz, &blksz,
|
|
&tftp_addr, (UINT8 *)full_path, NULL, nobuffer);
|
|
if (efi_status == EFI_BUFFER_TOO_SMALL) {
|
|
/* try again, doubling buf size */
|
|
*bufsiz *= 2;
|
|
FreePool(*buffer);
|
|
*buffer = AllocatePool(*bufsiz);
|
|
if (!*buffer)
|
|
return EFI_OUT_OF_RESOURCES;
|
|
goto try_again;
|
|
}
|
|
|
|
if (EFI_ERROR(efi_status)) {
|
|
if (pxe->Mode->TftpErrorReceived) {
|
|
if (~flags & SUPPRESS_NETBOOT_OPEN_FAILURE_NOISE)
|
|
console_print(L"TFTP error %u: %a\n",
|
|
pxe->Mode->TftpError.ErrorCode,
|
|
pxe->Mode->TftpError.ErrorString);
|
|
|
|
efi_status = status_from_error(pxe->Mode->TftpError.ErrorCode);
|
|
} else if (efi_status == EFI_TFTP_ERROR) {
|
|
/*
|
|
* Unfortunately, some firmware doesn't fill in the
|
|
* error details. Treat all TFTP errors like file not
|
|
* found so shim falls back to the default loader.
|
|
*
|
|
* https://github.com/tianocore/edk2/pull/6287
|
|
*/
|
|
if (~flags & SUPPRESS_NETBOOT_OPEN_FAILURE_NOISE)
|
|
console_print(L"Unknown TFTP error, treating "
|
|
"as file not found\n");
|
|
efi_status = EFI_NOT_FOUND;
|
|
}
|
|
|
|
if (*buffer) {
|
|
FreePool(*buffer);
|
|
}
|
|
}
|
|
return efi_status;
|
|
}
|