blob: 6b762d11840ff19137eeed474a1e7a23c31c29fb [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 "netboot.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <threads.h>
#include <unistd.h>
#include <fuchsia/device/manager/c/fidl.h>
#include <inet6/inet6.h>
#include <inet6/netifc.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/directory.h>
#include <zircon/boot/netboot.h>
#include <zircon/process.h>
#include <zircon/processargs.h>
#include <zircon/syscalls.h>
#include "netsvc.h"
#include "netcp.h"
#include "paver.h"
#include "zbi.h"
static uint32_t last_cookie = 0;
static uint32_t last_cmd = 0;
static uint32_t last_arg = 0;
static uint32_t last_ack_cmd = 0;
static uint32_t last_ack_arg = 0;
#define PAGE_ROUNDUP(x) ((x + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1))
#define MAX_ADVERTISE_DATA_LEN 256
bool xfer_active = false;
struct nbfilecontainer_t {
nbfile file;
zx_handle_t data; // handle to vmo that backs netbootfile.
};
static nbfilecontainer_t nbkernel;
static nbfilecontainer_t nbbootdata;
static nbfilecontainer_t nbcmdline;
// Pointer to the currently active transfer.
static nbfile* active;
static zx_status_t nbfilecontainer_init(size_t size, nbfilecontainer_t* target) {
zx_status_t st = ZX_OK;
assert(target);
// De-init the container if it's already initialized.
if (target->file.data) {
// For now, I can't see a valid reason that a client would send the same
// filename twice.
// We'll handle this case gracefully, but we'll print a warning to the
// console just in case it was a mistake.
printf("netbootloader: warning, reusing a previously initialized container\n");
// Unmap the vmo from the address space.
st = zx_vmar_unmap(zx_vmar_root_self(), reinterpret_cast<uintptr_t>(target->file.data),
target->file.size);
if (st != ZX_OK) {
printf("netbootloader: failed to unmap existing vmo, st = %d\n", st);
return st;
}
zx_handle_close(target->data);
target->file.offset = 0;
target->file.size = 0;
target->file.data = 0;
}
st = zx_vmo_create(size, 0, &target->data);
if (st != ZX_OK) {
printf("netbootloader: Could not create a netboot vmo of size = %lu "
"retcode = %d\n",
size, st);
return st;
}
zx_object_set_property(target->data, ZX_PROP_NAME, "netboot", 7);
uintptr_t buffer;
st = zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, 0, target->data, 0,
size, &buffer);
if (st != ZX_OK) {
printf("netbootloader: failed to map data vmo for buffer, st = %d\n", st);
zx_handle_close(target->data);
return st;
}
target->file.offset = 0;
target->file.size = size;
target->file.data = reinterpret_cast<uint8_t*>(buffer);
return ZX_OK;
}
nbfile* netboot_get_buffer(const char* name, size_t size) {
zx_status_t st = ZX_OK;
nbfilecontainer_t* result;
if (!strcmp(name, NB_KERNEL_FILENAME)) {
result = &nbkernel;
} else if (!strcmp(name, NB_RAMDISK_FILENAME)) {
result = &nbbootdata;
} else if (!strcmp(name, NB_CMDLINE_FILENAME)) {
result = &nbcmdline;
} else {
return NULL;
}
st = nbfilecontainer_init(size, result);
if (st != ZX_OK) {
printf("netbootloader: failed to initialize file container for "
"file = '%s', retcode = %d\n",
name, st);
return NULL;
}
return &result->file;
}
void netboot_advertise(const char* nodename) {
// Don't advertise if a transfer is active.
if (xfer_active)
return;
struct {
nbmsg msg;
char data[MAX_ADVERTISE_DATA_LEN];
} packet_data;
nbmsg* msg = &packet_data.msg;
msg->magic = NB_MAGIC;
msg->cookie = 0;
msg->cmd = NB_ADVERTISE;
msg->arg = NB_VERSION_CURRENT;
snprintf(packet_data.data, MAX_ADVERTISE_DATA_LEN, "version=%s;nodename=%s",
BOOTLOADER_VERSION, nodename);
const size_t data_len = strlen(packet_data.data) + 1;
udp6_send(&packet_data, sizeof(nbmsg) + data_len, &ip6_ll_all_nodes, NB_ADVERT_PORT, NB_SERVER_PORT,
false);
}
static void nb_open(const char* filename, uint32_t cookie, uint32_t arg, const ip6_addr_t* saddr,
uint16_t sport, uint16_t dport) {
nbmsg m;
m.magic = NB_MAGIC;
m.cookie = cookie;
m.cmd = NB_ACK;
m.arg = netcp_open(filename, arg, NULL);
udp6_send(&m, sizeof(m), saddr, sport, dport, false);
}
struct netfilemsg {
nbmsg hdr;
uint8_t data[1024];
};
static void nb_read(uint32_t cookie, uint32_t arg, const ip6_addr_t* saddr, uint16_t sport,
uint16_t dport) {
static netfilemsg m = {
.hdr =
{
.magic = NB_MAGIC,
.cookie = 0,
.cmd = NB_ACK,
.arg = 0,
.data = {},
},
.data = {},
};
static size_t msg_size = 0;
static uint32_t blocknum = static_cast<uint32_t>(-1);
if (arg == blocknum) {
// Request to resend last message, verify that the cookie is unchanged
if (cookie != m.hdr.cookie) {
m.hdr.arg = -EIO;
m.hdr.cookie = cookie;
msg_size = sizeof(m.hdr);
}
} else if (arg == 0 || arg == blocknum + 1) {
ssize_t result = netcp_read(&m.data, sizeof(m.data));
if (result < 0) {
m.hdr.arg = static_cast<uint32_t>(result);
msg_size = sizeof(m.hdr);
} else {
// Note that the response does not use actual size as the argument,
// it matches the *requested* size. Actual size can be determined by
// the packet size.
m.hdr.arg = arg;
msg_size = sizeof(m.hdr) + result;
}
m.hdr.cookie = cookie;
blocknum = arg;
} else {
// Ignore bogus read requests -- host will timeout if they're confused
return;
}
udp6_send(&m, msg_size, saddr, sport, dport, false);
}
static void nb_write(const char* data, size_t len, uint32_t cookie, uint32_t arg,
const ip6_addr_t* saddr, uint16_t sport, uint16_t dport) {
static nbmsg m = {
.magic = NB_MAGIC,
.cookie = 0,
.cmd = NB_ACK,
.arg = 0,
.data = {},
};
static uint32_t blocknum = static_cast<uint32_t>(-1);
if (arg == blocknum) {
// Request to repeat last write, verify that cookie is unchanged
if (cookie != m.cookie) {
m.arg = -EIO;
}
} else if (arg == 0 || arg == blocknum + 1) {
ssize_t result = netcp_write(data, len);
m.arg = static_cast<uint32_t>(result > 0 ? 0 : result);
blocknum = arg;
}
m.cookie = cookie;
udp6_send(&m, sizeof(m), saddr, sport, dport, false);
}
static void nb_close(uint32_t cookie, const ip6_addr_t* saddr, uint16_t sport, uint16_t dport) {
nbmsg m;
m.magic = NB_MAGIC;
m.cookie = cookie;
m.cmd = NB_ACK;
m.arg = netcp_close();
udp6_send(&m, sizeof(m), saddr, sport, dport, false);
}
static zx_status_t do_dmctl_mexec() {
zx_handle_t kernel, bootdata;
// TODO(scottmg): range check nbcmdline.file.size rather than just casting.
zx_status_t status =
netboot_prepare_zbi(nbkernel.data, nbbootdata.data, nbcmdline.file.data,
static_cast<uint32_t>(nbcmdline.file.size), &kernel, &bootdata);
if (status != ZX_OK) {
return status;
}
zx_handle_t wait_handle;
status = zx_handle_duplicate(kernel, ZX_RIGHT_SAME_RIGHTS, &wait_handle);
if (status != ZX_OK) {
return status;
}
int fd = open("/dev/misc/dmctl", O_WRONLY);
if (fd < 0) {
return ZX_ERR_INTERNAL;
}
zx_handle_t dmctl;
status = fdio_get_service_handle(fd, &dmctl);
if (status != ZX_OK) {
return status;
}
status = fuchsia_device_manager_ExternalControllerPerformMexec(dmctl, kernel, bootdata);
zx_handle_close(dmctl);
if (status != ZX_OK) {
return status;
}
status = zx_object_wait_one(wait_handle, ZX_USER_SIGNAL_0, ZX_TIME_INFINITE, NULL);
zx_handle_close(wait_handle);
if (status != ZX_OK) {
return status;
}
// if we get here, mexec failed
return ZX_ERR_INTERNAL;
}
static zx_status_t reboot() {
zx_handle_t local, remote;
zx_status_t status = zx_channel_create(0, &local, &remote);
if (status != ZX_OK) {
return ZX_ERR_INTERNAL;
}
status = fdio_service_connect("/svc/fuchsia.device.manager.Administrator", remote);
if (status != ZX_OK) {
zx_handle_close(local);
return ZX_ERR_INTERNAL;
}
zx_status_t call_status;
status = fuchsia_device_manager_AdministratorSuspend(local,
fuchsia_device_manager_SUSPEND_FLAG_REBOOT,
&call_status);
zx_handle_close(local);
if (status != ZX_OK) {
return status;
}
return call_status;
}
static void bootloader_recv(void* data, size_t len, const ip6_addr_t* daddr, uint16_t dport,
const ip6_addr_t* saddr, uint16_t sport) {
nbmsg* msg = reinterpret_cast<nbmsg*>(data);
nbmsg ack;
bool do_transmit = true;
bool do_boot = false;
bool do_reboot = false;
if (dport != NB_SERVER_PORT)
return;
if (len < sizeof(nbmsg))
return;
len -= sizeof(nbmsg);
if ((last_cookie == msg->cookie) && (last_cmd == msg->cmd) && (last_arg == msg->arg)) {
// host must have missed the ack. resend
ack.magic = NB_MAGIC;
ack.cookie = last_cookie;
ack.cmd = last_ack_cmd;
ack.arg = last_ack_arg;
goto transmit;
}
ack.cmd = NB_ACK;
ack.arg = 0;
switch (msg->cmd) {
case NB_COMMAND:
if (len == 0)
return;
msg->data[len - 1] = 0;
break;
case NB_SEND_FILE:
xfer_active = true;
if (len == 0)
return;
msg->data[len - 1] = 0;
for (size_t i = 0; i < (len - 1); i++) {
if ((msg->data[i] < ' ') || (msg->data[i] > 127)) {
msg->data[i] = '.';
}
}
active = netboot_get_buffer(reinterpret_cast<const char*>(msg->data), msg->arg);
if (active) {
active->offset = 0;
ack.arg = msg->arg;
size_t prefix_len = strlen(NB_FILENAME_PREFIX);
const char* filename;
if (!strncmp(reinterpret_cast<char*>(msg->data), NB_FILENAME_PREFIX, prefix_len)) {
filename = &(reinterpret_cast<const char*>(msg->data))[prefix_len];
} else {
filename = reinterpret_cast<const char*>(msg->data);
}
printf("netboot: Receive File '%s'...\n", filename);
} else {
printf("netboot: Rejected File '%s'...\n", reinterpret_cast<char*>(msg->data));
ack.cmd = NB_ERROR_BAD_FILE;
}
break;
case NB_DATA:
case NB_LAST_DATA:
xfer_active = true;
if (active == 0) {
printf("netboot: > received chunk before NB_FILE\n");
return;
}
if (msg->arg != active->offset) {
// printf("netboot: < received chunk at offset %d but current offset is %zu\n",
// msg->arg, active->offset);
ack.arg = static_cast<uint32_t>(active->offset);
ack.cmd = NB_ACK;
} else if ((active->offset + len) > active->size) {
ack.cmd = NB_ERROR_TOO_LARGE;
ack.arg = msg->arg;
} else {
memcpy(active->data + active->offset, msg->data, len);
active->offset += len;
ack.cmd = msg->cmd == NB_LAST_DATA ? NB_FILE_RECEIVED : NB_ACK;
if (msg->cmd != NB_LAST_DATA) {
do_transmit = false;
} else {
xfer_active = false;
}
}
break;
case NB_BOOT:
// Wait for the paver to complete
while (netsvc::Paver::Get()->InProgress()) {
thrd_yield();
}
if (netsvc::Paver::Get()->exit_code() != 0) {
printf("netboot: detected paver error: %d\n", netsvc::Paver::Get()->exit_code());
netsvc::Paver::Get()->reset_exit_code();
break;
}
do_boot = true;
printf("netboot: Boot Kernel...\n");
break;
case NB_REBOOT:
// Wait for the paver to complete
while (netsvc::Paver::Get()->InProgress()) {
thrd_yield();
}
if (netsvc::Paver::Get()->exit_code() != 0) {
printf("netboot: detected paver error: %d\n", netsvc::Paver::Get()->exit_code());
netsvc::Paver::Get()->reset_exit_code();
break;
}
do_reboot = true;
printf("netboot: Reboot ...\n");
break;
default:
// We don't have a handler for this command, let netsvc handle it.
do_transmit = false;
}
last_cookie = msg->cookie;
last_cmd = msg->cmd;
last_arg = msg->arg;
last_ack_cmd = ack.cmd;
last_ack_arg = ack.arg;
ack.cookie = msg->cookie;
ack.magic = NB_MAGIC;
transmit:
if (do_transmit) {
udp6_send(&ack, sizeof(ack), saddr, sport, NB_SERVER_PORT, false);
}
if (do_boot) {
zx_status_t status = do_dmctl_mexec();
if (status != ZX_OK) {
// TODO: This will return before the system actually mexecs.
// We can't pass an event to wait on here because fdio
// has a limit of 3 handles, and we're already using
// all 3 to pass boot parameters.
printf("netboot: Boot failed. status = %d\n", status);
}
}
if (do_reboot) {
zx_status_t status = reboot();
if (status != ZX_OK) {
printf("netboot: Reboot failed. status = %d\n", status);
}
}
}
void netboot_recv(void* data, size_t len, bool is_mcast, const ip6_addr_t* daddr, uint16_t dport,
const ip6_addr_t* saddr, uint16_t sport) {
nbmsg* msg = reinterpret_cast<nbmsg*>(data);
// Not enough bytes to be a message
if ((len < sizeof(*msg)) || (msg->magic != NB_MAGIC)) {
return;
}
len -= sizeof(*msg);
if (len && msg->cmd != NB_DATA && msg->cmd != NB_LAST_DATA) {
msg->data[len - 1] = '\0';
}
switch (msg->cmd) {
case NB_QUERY: {
if (strcmp(reinterpret_cast<char*>(msg->data), "*") &&
strcmp(reinterpret_cast<char*>(msg->data), nodename())) {
break;
}
size_t dlen = strlen(nodename()) + 1;
char buf[1024 + sizeof(nbmsg)];
if ((dlen + sizeof(nbmsg)) > sizeof(buf)) {
return;
}
msg->cmd = NB_ACK;
memcpy(buf, msg, sizeof(nbmsg));
memcpy(buf + sizeof(nbmsg), nodename(), dlen);
udp6_send(buf, sizeof(nbmsg) + dlen, saddr, sport, dport, false);
break;
}
case NB_SHELL_CMD:
if (!is_mcast) {
netboot_run_cmd(reinterpret_cast<char*>(msg->data));
return;
}
break;
case NB_OPEN:
nb_open(reinterpret_cast<char*>(msg->data), msg->cookie, msg->arg, saddr, sport, dport);
break;
case NB_READ:
nb_read(msg->cookie, msg->arg, saddr, sport, dport);
break;
case NB_WRITE:
len--; // NB NUL-terminator is not part of the data
nb_write(reinterpret_cast<char*>(msg->data), len, msg->cookie, msg->arg, saddr, sport,
dport);
break;
case NB_CLOSE:
nb_close(msg->cookie, saddr, sport, dport);
break;
default:
// If the bootloader is enabled, then let it have a crack at the
// incoming packets as well.
if (netbootloader()) {
bootloader_recv(data, len + sizeof(nbmsg), daddr, dport, saddr, sport);
}
}
}