/*
* GRUB -- GRand Unified Bootloader
* Copyright (C) 2010,2011 Free Software Foundation, Inc.
* Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
*
* GRUB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* GRUB is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GRUB. If not, see <http://www.gnu.org/licenses/>.
*/
#include <grub/net/netbuff.h>
#include <grub/dl.h>
#include <grub/net.h>
#include <grub/time.h>
#include <grub/efi/api.h>
#include <grub/efi/efi.h>
GRUB_MOD_LICENSE ("GPLv3+");
#ifdef __sun__
#define MULTI_CARD_UNDI_BROKEN_WORKAROUND
#define WORKAROUND_BROKEN_INTEL_DRIVERS
#include <grub/env.h>
#define DELAY_ENV_VAR "efinet_delay"
#define DEFAULT_DELAY 1
static unsigned long efinet_receive_delay = DEFAULT_DELAY;
#endif
/* GUID. */
static grub_efi_guid_t net_io_guid = GRUB_EFI_SIMPLE_NETWORK_GUID;
static grub_efi_guid_t pxe_io_guid = GRUB_EFI_PXE_GUID;
#define EFINET_FILTERS (EFI_SNP_FILTER_UNICAST|EFI_SNP_FILTER_BROADCAST)
static grub_err_t
send_card_buffer (const struct grub_net_card *dev,
struct grub_net_buff *pack)
{
grub_efi_status_t st;
grub_efi_simple_network_t *net = dev->efi_net;
grub_uint64_t limit_time = grub_get_time_ms () + 4000;
if (! net)
return grub_error (GRUB_ERR_IO, "EFI network not initialized");
st = efi_call_7 (net->transmit, net, 0, (pack->tail - pack->data),
pack->data, NULL, NULL, NULL);
if (st != GRUB_EFI_SUCCESS)
return grub_error (GRUB_ERR_IO, "couldn't send network packet");
while (1)
{
void *txbuf = NULL;
st = efi_call_3 (net->get_status, net, 0, &txbuf);
if (st != GRUB_EFI_SUCCESS)
return grub_error (GRUB_ERR_IO, "couldn't send network packet");
if (txbuf)
return GRUB_ERR_NONE;
if (limit_time < grub_get_time_ms ())
return grub_error (GRUB_ERR_TIMEOUT, "couldn't send network packet");
}
}
static struct grub_net_buff *
get_card_packet (const struct grub_net_card *dev)
{
grub_efi_simple_network_t *net = dev->efi_net;
grub_err_t err;
grub_efi_status_t st;
grub_efi_uintn_t bufsize = 1536;
struct grub_net_buff *nb;
if (! net)
return NULL;
#ifdef WORKAROUND_BROKEN_INTEL_DRIVERS
if (efinet_receive_delay > 0)
grub_millisleep((grub_uint32_t)efinet_receive_delay);
#endif
nb = grub_netbuff_alloc (bufsize);
if (!nb)
return NULL;
/* Reserve 2 bytes so that 2 + 14/18 bytes of ethernet header is divisible
by 4. So that IP header is aligned on 4 bytes. */
grub_netbuff_reserve (nb, 2);
if (!nb)
{
grub_netbuff_free (nb);
return NULL;
}
st = efi_call_7 (net->receive, net, NULL, &bufsize,
nb->data, NULL, NULL, NULL);
if (st == GRUB_EFI_BUFFER_TOO_SMALL)
{
grub_netbuff_free (nb);
bufsize = ALIGN_UP (bufsize, 32);
nb = grub_netbuff_alloc (bufsize);
if (!nb)
return NULL;
/* Reserve 2 bytes so that 2 + 14/18 bytes of ethernet header is divisible
by 4. So that IP header is aligned on 4 bytes. */
grub_netbuff_reserve (nb, 2);
if (!nb)
{
grub_netbuff_free (nb);
return NULL;
}
st = efi_call_7 (net->receive, net, NULL, &bufsize,
nb->data, NULL, NULL, NULL);
}
if (st != GRUB_EFI_SUCCESS)
{
grub_netbuff_free (nb);
return NULL;
}
err = grub_netbuff_put (nb, bufsize);
if (err)
{
grub_netbuff_free (nb);
return NULL;
}
return nb;
}
static void
efinet_close (const struct grub_net_card *dev)
{
grub_efi_simple_network_t *net = dev->efi_net;
if (net)
{
/* First we call shutdown(), then stop() */
grub_efi_status_t st = efi_call_1(net->shutdown, net);
if (st != GRUB_EFI_SUCCESS)
grub_dprintf("net", "Error calling shutdown() for efinet: 0x%lx\n",
(long)st);
st = efi_call_1(net->stop, net);
if (st != GRUB_EFI_SUCCESS)
grub_dprintf("net", "Error stop()ing efinet: 0x%lx\n",
(long)st);
st = efi_call_4(grub_efi_system_table->boot_services->close_protocol,
dev->efi_net, &net_io_guid, grub_efi_image_handle, dev->efi_handle);
if (st != GRUB_EFI_SUCCESS)
grub_dprintf("net", "Error closing protocol handle efnet: 0x%lx\n",
(long)st);
else
((struct grub_net_card *)dev)->efi_net = 0;
}
}
static struct grub_net_card_driver efidriver =
{
.name = "efinet",
.close = efinet_close,
.send = send_card_buffer,
.recv = get_card_packet
};
static int
setup_filters(grub_efi_simple_network_t *net, int exclusive)
{
grub_efi_status_t st;
/* If the current set of filters on the NIC doesn't match what we
want (constained by the set of supported filters, of course),
set them up now. */
if ((net->mode->receive_filter_setting & EFINET_FILTERS) !=
(EFINET_FILTERS & net->mode->receive_filter_mask))
{
/* If we're not exclusive, OR-in the required filters, just
in case we're sharing the interface. Of course, this is
no guarantee that these will be respected when the other
consumer changes the flags, but this is the best we can
do for now. */
grub_uint32_t newfilters = (exclusive ? 0 :
net->mode->receive_filter_setting) |
(EFINET_FILTERS & net->mode->receive_filter_mask);
st = efi_call_6(net->receive_filters, net, newfilters,
net->mode->receive_filter_setting & ~(newfilters),
1, /* Reset multicast filters */
0, /* No multicast filters included */
0);
/* Check if setting the filters failed */
if (st != GRUB_EFI_SUCCESS ||
(net->mode->receive_filter_setting & EFINET_FILTERS) !=
(EFINET_FILTERS & net->mode->receive_filter_mask))
return -1;
}
return 0;
}
static void
grub_efinet_findcards (void)
{
grub_efi_uintn_t num_handles;
grub_efi_handle_t *handles;
grub_efi_handle_t *handle;
int i = 0;
/* Find handles which support the disk io interface. */
handles = grub_efi_locate_handle (GRUB_EFI_BY_PROTOCOL, &net_io_guid,
0, &num_handles);
if (! handles)
return;
for (handle = handles; num_handles--; handle++)
{
grub_efi_simple_network_t *net;
struct grub_net_card *card;
#ifdef MULTI_CARD_UNDI_BROKEN_WORKAROUND
struct grub_efi_pxe *pxe;
volatile struct grub_efi_pxe_mode *pxe_mode;
#endif
/* We cannot ask for the exclusive handle here, otherwise we'd
blow away pxe, who may be holding our cached ack that we'll
retrieve later */
net = grub_efi_open_protocol (*handle, &net_io_guid,
GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (! net)
/* This should not happen... Why? */
continue;
#ifdef MULTI_CARD_UNDI_BROKEN_WORKAROUND
pxe = grub_efi_open_protocol (*handle, &pxe_io_guid,
GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (!pxe)
continue;
pxe_mode = pxe->mode;
if (!pxe_mode->dhcp_ack_recvd)
continue;
#endif
if (net->mode->state == GRUB_EFI_NETWORK_STOPPED
&& efi_call_1 (net->start, net) != GRUB_EFI_SUCCESS)
continue;
if (net->mode->state == GRUB_EFI_NETWORK_STOPPED)
continue;
if (net->mode->state == GRUB_EFI_NETWORK_STARTED
&& efi_call_3 (net->initialize, net, 0, 0) != GRUB_EFI_SUCCESS)
continue;
#if !defined(MULTI_CARD_UNDI_BROKEN_WORKAROUND)
/* Set up receive filters here */
setup_filters(net, 0);
#endif
card = grub_zalloc (sizeof (struct grub_net_card));
if (!card)
{
grub_print_error ();
grub_free (handles);
return;
}
card->name = grub_xasprintf ("efinet%d", i++);
card->driver = &efidriver;
card->flags = 0;
card->default_address.type = GRUB_NET_LINK_LEVEL_PROTOCOL_ETHERNET;
card->mtu = net->mode->max_packet_size;
grub_memcpy (card->default_address.mac,
(void *)net->mode->current_address,
sizeof (card->default_address.mac));
card->efi_net = net;
card->efi_handle = *handle;
grub_net_card_register (card);
#ifdef MULTI_CARD_UNDI_BROKEN_WORKAROUND
/* We only support one active NIC at a time */
break;
#endif
}
grub_free (handles);
}
/*
* This function assumes that card->efi_net was already initialized to
* a protocol handle for SNP.
*/
static int
init_efinet(struct grub_net_card *card)
{
grub_efi_simple_network_t *net;
int exclusive;
/* Open the SNP exclusively, otherwise upper layer drivers such as MNP may
steal our packets before we've had a chance to see them. */
net = grub_efi_open_protocol_full (card->efi_handle, &net_io_guid,
grub_efi_image_handle, card->efi_handle,
GRUB_EFI_OPEN_PROTOCOL_BY_DRIVER|GRUB_EFI_OPEN_PROTOCOL_BY_EXCLUSIVE);
/* If we cannot get exclusive use of the NIC, print a warning, but
try to continue */
if (! net)
{
grub_printf("WARNING: Could not open efinet exclusively. Packet loss\n");
grub_printf(" May occur if there is contention with other UEFI\n");
grub_printf(" drivers (network I/O may be slower).\n");
net = card->efi_net;
exclusive = 0;
}
else
{
card->efi_net = net;
exclusive = 1;
/* Once we gain exclusive access, the SNP will probably be shut down.
reinitialize it here. */
if (net->mode->state == GRUB_EFI_NETWORK_STOPPED
&& efi_call_1 (net->start, net) != GRUB_EFI_SUCCESS)
return -1;
if (net->mode->state == GRUB_EFI_NETWORK_STOPPED)
return -1;
if (net->mode->state == GRUB_EFI_NETWORK_STARTED
&& efi_call_3 (net->initialize, net, 0, 0) != GRUB_EFI_SUCCESS)
return -1;
}
if (setup_filters(net, exclusive) != 0)
return -1;
return 0;
}
static void
grub_efi_net_config_real (grub_efi_handle_t hnd, char **device,
char **path)
{
struct grub_net_card *card;
grub_efi_device_path_t *dp;
dp = grub_efi_get_device_path (hnd);
if (! dp)
return;
FOR_NET_CARDS (card)
{
grub_efi_device_path_t *cdp;
struct grub_efi_pxe *pxe;
volatile struct grub_efi_pxe_mode *pxe_mode;
if (card->driver != &efidriver)
continue;
cdp = grub_efi_get_device_path (card->efi_handle);
if (! cdp)
continue;
if (grub_efi_compare_device_paths (dp, cdp) != 0)
continue;
pxe = grub_efi_open_protocol (hnd, &pxe_io_guid,
GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (! pxe)
return;
pxe_mode = pxe->mode;
grub_net_configure_by_dhcp_ack (card->name, card, 0,
(struct grub_net_bootp_packet *)
&pxe_mode->dhcp_ack,
sizeof (pxe_mode->dhcp_ack),
1, device, path);
/* Fully initialize SNP by gaining exclusive access, if possible,
and setting up the receive filters properly. */
if (init_efinet(card) != 0)
grub_printf("Could not initialize the NIC via UEFI\n");
return;
}
}
#ifdef WORKAROUND_BROKEN_INTEL_DRIVERS
static char *
efinet_change_delay(struct grub_env_var *var __attribute__ ((unused)),
const char *val)
{
unsigned long receive_delay;
if (! val)
return NULL;
receive_delay = grub_strtoul(val, 0, 0);
if (receive_delay > 0 && receive_delay <= 1000)
efinet_receive_delay = receive_delay;
return grub_strdup(val);
}
#endif
GRUB_MOD_INIT(efinet)
{
grub_efinet_findcards ();
grub_efi_net_config = grub_efi_net_config_real;
#ifdef WORKAROUND_BROKEN_INTEL_DRIVERS
grub_register_variable_hook (DELAY_ENV_VAR, 0, efinet_change_delay);
#endif
}
GRUB_MOD_FINI(efinet)
{
struct grub_net_card *card, *next;
#ifdef WORKAROUND_BROKEN_INTEL_DRIVERS
grub_register_variable_hook (DELAY_ENV_VAR, 0, 0);
#endif
FOR_NET_CARDS_SAFE (card, next)
if (card->driver == &efidriver)
grub_net_card_unregister (card);
}